1. 前言
在并发过程中,多个线程或 goroutine 可能同时操作同一内存区域,导致出现竞争问题。为保持内存一致性,Go 的 sync 包提供了常见的并发编程原语。其中包括:Mutex、RWMutex、WaitGroup、Once 和 Pool 等。
2. RWMutex
RWMutex 适用于读多写少的场景,在同一时刻,有且只能有一个 goroutine 获取该锁。writer 比 reader 获取锁的优先级更高,writer 尝试获取锁时,将会阻塞所有新到来的 reader,直至 writer 获取到锁且释放掉。
2.1 使用
package main
import (
"sync"
)
func main() {
mu := sync.RWMutex{}
mu.RLock()
fmt.Println("Read lock success")
mu.RUnlock()
mu.Lock()
fmt.Println("Write lock success")
mu.Unlock()
}
输出结果如下:
Read lock success
Write lock success
2.2 实现原理
移除打印输出内容,生成对应的汇编内容如下所示:
TEXT "".main(SB) gofile../Users/liangjinsi/Documents/workspace/wenote/go/source/sync/main.go
main.go:7 0x602 493b6610 CMPQ 0x10(R14), SP
main.go:7 0x606 7669 JBE 0x671
main.go:7 0x608 4883ec20 SUBQ $0x20, SP
main.go:7 0x60c 48896c2418 MOVQ BP, 0x18(SP)
main.go:7 0x611 488d6c2418 LEAQ 0x18(SP), BP
main.go:8 0x616 488d0500000000 LEAQ 0(IP), AX [3:7]R_PCREL:type.sync.RWMutex
main.go:8 0x61d 0f1f440000 NOPL 0(AX)(AX*1)
main.go:8 0x622 e800000000 CALL 0x627 [1:5]R_CALL:runtime.newobject<1>
main.go:8 0x627 4889442410 MOVQ AX, 0x10(SP)
main.go:8 0x62c 48c70000000000 MOVQ $0x0, 0(AX)
main.go:8 0x633 488d4808 LEAQ 0x8(AX), CX
main.go:8 0x637 440f1139 MOVUPS X15, 0(CX)
main.go:9 0x63b 488b442410 MOVQ 0x10(SP), AX
main.go:9 0x640 6690 NOPW
main.go:9 0x642 e800000000 CALL 0x647 [1:5]R_CALL:sync.(*RWMutex).RLock
main.go:10 0x647 488b442410 MOVQ 0x10(SP), AX
main.go:10 0x64c e800000000 CALL 0x651 [1:5]R_CALL:sync.(*RWMutex).RUnlock
main.go:11 0x651 488b442410 MOVQ 0x10(SP), AX
main.go:11 0x656 e800000000 CALL 0x65b [1:5]R_CALL:sync.(*RWMutex).Lock
main.go:12 0x65b 488b442410 MOVQ 0x10(SP), AX
main.go:12 0x660 6690 NOPW
main.go:12 0x662 e800000000 CALL 0x667 [1:5]R_CALL:sync.(*RWMutex).Unlock
main.go:13 0x667 488b6c2418 MOVQ 0x18(SP), BP
main.go:13 0x66c 4883c420 ADDQ $0x20, SP
main.go:13 0x670 c3 RET
main.go:7 0x671 e800000000 CALL 0x676 [1:5]R_CALL:runtime.morestack_noctxt
main.go:7 0x676 eb8a JMP "".main(SB)
编译器将 sync.RWMutex 解析成 type.sync.RWMutex 类型, RWMutext 是不可复制的
2.2.1 RWMutex 定义
type.sync.RWMutex 结构定义在 /src/sync/rwmutex.go 文件中,具体内容如下:
const rwmutexMaxReaders = 1 << 30
type RWMutex struct {
w Mutex // 互斥锁,保护写操作
writerSem uint32 // 写信号
readerSem uint32 // 读信号
readerCount int32 // 读操作计数
readerWait int32 // writer 等待完成的 reader 数量
}
- rwmutexMaxReaders 表示最大同时支持有 1 << 30 个 reader 在进行读取操作。
- readerCount 记录当前存在多少个 reader 进行读取操作
- readerWait 表示当 writer 尝试获取锁时,需要等待多少个 reader 读取完成后才能获取锁。
- writerSem、readerSem 分别为写信号和读信号量,在 RWMutex 中主要通过 runtime_SemacquireMutex 和 runtime_Semrelease 去获取或释放信号量,在源码中两个 function 的描述如下:
// SemacquireMutex is like Semacquire, but for profiling contended Mutexes.
// If lifo is true, queue waiter at the head of wait queue.
// skipframes is the number of frames to omit during tracing, counting from
// runtime_SemacquireMutex's caller.
func runtime_SemacquireMutex(s *uint32, lifo bool, skipframes int)
// Semrelease atomically increments *s and notifies a waiting goroutine
// if one is blocked in Semacquire.
// It is intended as a simple wakeup primitive for use by the synchronization
// library and should not be used directly.
// If handoff is true, pass count directly to the first waiter.
// skipframes is the number of frames to omit during tracing, counting from
// runtime_Semrelease's caller.
func runtime_Semrelease(s *uint32, handoff bool, skipframes int)
2.2.2 RWMutex.Lock
RWMutex.Lock 实现逻辑如下所示:
func (rw *RWMutex) Lock() {
if race.Enabled {
_ = rw.w.state
race.Disable()
}
// 先解决和其它写者的竞争
rw.w.Lock()
// Announce to readers there is a pending writer.
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
// Wait for active readers.
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false, 0)
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
race.Acquire(unsafe.Pointer(&rw.writerSem))
}
}
执行流程如下图所示:
简单概括就是:
- 写操作先通过 mutex 互斥竞争获取锁
- 若竞争到锁,则将 readerCount 的数量取反,并将当前的 readerCount 数记录到 readerWait, 阻塞新到达的 reader
- 当 r != 0 或 readerWait 数和 当前已被阻塞的 reader 数不一致时,表示存在读操作,则等待所有唤醒的 reader 处理完后,对 writerSem 信号量进行加锁。
2.2.3 RWMutex.Unlock
RWMutex.Unlock 实现逻辑如下:
func (rw *RWMutex) Unlock() {
if race.Enabled {
_ = rw.w.state
race.Release(unsafe.Pointer(&rw.readerSem))
race.Disable()
}
// 通知读者已经没有活跃的写者了,恢复可以读锁锁定
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
if r >= rwmutexMaxReaders {
race.Enable()
throw("sync: Unlock of unlocked RWMutex")
}
// 唤醒全部被阻塞的读操作
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
// 释放互斥锁,允许下一个写操作
rw.w.Unlock()
if race.Enabled {
race.Enable()
}
}
流程描述如下图所示:
简单概括:
- 获取被阻塞的 reader 数量,依次唤醒对应的 reader
- 释放 mutex 锁
2.2.4 RWMutex.RLock
RWMutex.RLock 实现逻辑如下:
func (rw *RWMutex) RLock() {
if race.Enabled {
_ = rw.w.state
race.Disable()
}
// reader 数量加 1,若 readerCount 为负值,表示此时有 writer 等待请求锁
// writer 的优先级高,所以会把后来的 reader 阻塞
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// 等待写锁释放
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
}
}
流程描述如下:
- reader 想获取读锁时,会对 readerCount 进行加一,若加一后 readerCount 数仍小于 0,则表示当前有 writer 在执行中,将会堵塞 reader goroutine 直至 writer 释放。
2.2.5 RWMutex.RUnlock
RWMutex.RUnlock 实现逻辑如下:
func (rw *RWMutex) RUnlock() {
if race.Enabled {
_ = rw.w.state
race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
race.Disable()
}
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
// Outlined slow-path to allow the fast-path to be inlined
rw.rUnlockSlow(r)
}
if race.Enabled {
race.Enable()
}
}
func (rw *RWMutex) rUnlockSlow(r int32) {
// 仍有读操作的时候获得了写锁,就会报错
if r+1 == 0 || r+1 == -rwmutexMaxReaders {
race.Enable()
throw("sync: RUnlock of unlocked RWMutex")
}
// 如果被 writer 阻塞的 reader 数为 0,表示所有的 reader 都释放了
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
// The last reader unblocks the writer.
runtime_Semrelease(&rw.writerSem, false, 1)
}
}
流程描述如下:
- 若无写锁,那么 r >= 0 直接放行,称为快路径;
- 若有写尝试获取锁中,那么进入慢路径。
- 若读操作数量已经超过预设值,或仍有读操作进行中加了写锁,则异常。
- readerWait 减1,若 readerWait 为0,表示已经没有活跃的 reader ,此时释放 writeSem 信号,让获取到写锁的 writer 进入锁定状态。