Go语言修炼(十一):Go语言的读写锁是怎么工作的?
前言
在Go语言的并发编程世界中,锁机制是确保数据一致性和线程安全的重要工具。随着应用程序对并发性能要求的不断提升,传统的互斥锁(Mutex)虽然简单有效,但在某些场景下可能会成为性能瓶颈。
为了优化这种情况,Go语言引入了读写锁(sync.RWMutex
),它允许多个goroutine同时读取数据,但写入时则保持独占访问,从而在不牺牲线程安全的前提下,提高了程序的并发性能。
读写锁的设计思想源于对数据访问模式的细致观察:在大多数并发应用场景中,数据的读取操作远多于写入操作。因此,读写锁通过允许多个读者同时访问数据,而在有写入者时阻塞所有其他读写操作,来实现对共享资源的更高效管理。这种设计既保证了数据的一致性,又最大化了资源的利用率。
读写锁的原理和数据结构
读写锁的原理可以概括为:
- 每个锁分为读锁和写锁,读锁为共享锁,写锁是互斥锁。
- 没有加写锁时,多个协程都可以加读锁
- 加了写锁时,无法加读锁,读协程排队等待
- 加了读锁时,无法加写锁,写协程排队等待
Go语言中的读写锁RWMutex的数据结构在sync/rwmutex.go
中:
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount atomic.Int32 // number of pending readers
readerWait atomic.Int32 // number of departing readers
}
各个字段的解释如下:
w
:被写协程持有的互斥锁writerSem
:写协程的等待队列readerSem
:读协程的等待队列readerCount
:- 正值:正在读的协程。
- 负值:加了写锁后,要加读锁的协程数量
readerWait
:写锁应该等待的读协程个数
读写锁的操作
加写锁
没有读协程的情况
状态:readerCount = 0
。
此时加写锁的步骤为:
- 竞争互斥锁
w
。 - 将
readCount=0
变成readerCount = -rwmutexMaxReaders
常量。
有读协程的情况
状态:readerCount > 0
。例如,此时readCount=3
。
此时加写锁的步骤为:
- 竞争互斥锁
w
。 - 将
readCount = 3-rwmutexMaxReaders
。表示后面协程无法加读锁,并且前面有3个读写在等待。 - 计算需要等待多少个读协程释放:
readerWait = 3
。 - 如果需要等待读协程释放,陷入
writerSem
休眠。
源码分析
查看sync/rwmutex.go
,省略部分逻辑:
func (rw *RWMutex) Lock() {
// First, resolve competition with other writers.
rw.w.Lock()
// Announce to readers there is a pending writer.
r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
// Wait for active readers.
if r != 0 && rw.readerWait.Add(r) != 0 {
// r!=0,进入写协程休眠队列
runtime_SemacquireRWMutex(&rw.writerSem, false, 0)
}
// r = 0,直接加写锁成功
}
解写锁
状态:w
被持有,readerCount
为负数,readerWait=0
,读协程休眠队列存在若干协程。
- 将
readerCount + rwmutexMaxReaders
,允许读锁获取。 - 释放
readerSem
中等待的读协程。 - 解锁
mutex
源码分析
Go里面的源码注释已经很清晰:
func (rw *RWMutex) Unlock() {
// Announce to readers there is no active writer.
r := rw.readerCount.Add(rwmutexMaxReaders)
if r >= rwmutexMaxReaders {
race.Enable()
fatal("sync: Unlock of unlocked RWMutex")
}
// Unblock blocked readers, if any.
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
// Allow other writers to proceed.
rw.w.Unlock()
}
加读锁
readerCount > 0
状态:readerCount > 0
,例如readerCount = 2
此时加读锁的步骤为:
readerCount + 1
- 如果
readerCount > 0
,加锁成功
readerCount < 0
状态:readerCount < 0
,例如readerCount = 2- rwmutexMaxReaders
。
- 情况1:写锁在排队。
- 情况2:写锁在操作。
此时加读锁的步骤为:
readerCount + 1
- 如果
readerCount < 0
,说明被加了写锁,此时协程陷入readerSem
源码分析
func (rw *RWMutex) RLock() {
// readerCount + ,如果 > 0表示加锁成功
if rw.readerCount.Add(1) < 0 {
// A writer is pending, wait for it.
runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)
}
}
解读锁
算法步骤:
readerCount - 1
- 如果
readerCount>0
,解锁成功。 - 如果
readerCount < 0
,表示是负数,有写锁在排队。- 如果这个协程是
readerWait
的最后一个,则唤醒写协程。
- 如果这个协程是
源码分析:
func (rw *RWMutex) RUnlock() {
// readerCount - 1
if r := rw.readerCount.Add(-1); r < 0 {
// 有写协程在排队,需要进一步判断
rw.rUnlockSlow(r)
}
}
func (rw *RWMutex) rUnlockSlow(r int32) {
if r+1 == 0 || r+1 == -rwmutexMaxReaders {
race.Enable()
fatal("sync: RUnlock of unlocked RWMutex")
}
// 给readerWait-1,如果自己是最后一个读者,唤醒写协程
if rw.readerWait.Add(-1) == 0 {
runtime_Semrelease(&rw.writerSem, false, 1)
}
}
小结
通过对Go语言读写锁(sync.RWMutex
)工作机制的深入剖析,我们了解到它如何在保持线程安全的同时,通过允许并发读取来提升程序的性能。读写锁内部复杂的锁降级、锁升级以及状态转换逻辑,共同构建了一个既灵活又高效的并发控制机制。
在实际编程中,合理使用读写锁对于提升程序的并发能力和响应速度至关重要。然而,也需要注意到读写锁并非万能的,它在某些极端情况下(如写入操作非常频繁)可能会退化为互斥锁,从而失去其优势。因此,在设计并发程序时,开发者应根据实际的应用场景和需求,综合考虑各种锁机制的特点,选择最合适的同步原语。
- 感谢你赐予我前进的力量