在 Go 语言中,sync.RWMutex
读写锁的实现原理涉及多个关键部分,下面从其核心数据结构、获取读锁、获取写锁、释放读锁和释放写锁等方面详细介绍:
核心数据结构
type RWMutex struct {
w Mutex // 用于保护写操作的互斥锁
writerSem uint32 // 写操作的信号量
readerSem uint32 // 读操作的信号量
readerCount int32 // 当前读操作的数量
readerWait int32 // 写操作等待时需要等待的读操作数量
}
w
:这是一个普通的互斥锁(Mutex
),用于保护对readerCount
和readerWait
等字段的并发访问,确保在修改这些关键状态时的原子性。writerSem
:写操作的信号量,用于阻塞和唤醒等待的写操作 goroutine。当写操作需要等待读操作完成时,会通过这个信号量进入阻塞状态;当所有读操作完成后,会通过这个信号量唤醒等待的写操作。readerSem
:读操作的信号量,用于阻塞和唤醒等待的读操作 goroutine。当写操作正在进行时,新的读操作会通过这个信号量进入阻塞状态;当写操作完成后,会通过这个信号量唤醒等待的读操作。readerCount
:记录当前正在进行的读操作的数量。读操作开始时会增加这个计数器,读操作结束时会减少这个计数器。readerWait
:记录写操作等待时需要等待的读操作数量。当写操作请求锁时,会将当前的readerCount
值赋给readerWait
,表示写操作需要等待这些读操作完成。
获取读锁(RLock
方法)
func (rw *RWMutex) RLock() {
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// 有写操作正在进行或者有写操作在等待,当前读操作需要阻塞
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
}
- 调用
atomic.AddInt32(&rw.readerCount, 1)
原子地将readerCount
加 1,表示有一个新的读操作开始。 - 如果
readerCount
变为负数,说明有写操作正在进行或者有写操作在等待(因为在写操作请求锁时,会将readerCount
减去一个特定的常量rwmutexMaxReaders
,使其变为负数),此时当前读操作会调用runtime_SemacquireMutex
方法,通过readerSem
信号量进入阻塞状态,直到写操作完成。
获取写锁(Lock
方法)
func (rw *RWMutex) Lock() {
// 先获取互斥锁,保证对后续操作的独占访问
rw.w.Lock()
// 记录当前需要等待的读操作数量
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
// 有读操作正在进行,当前写操作需要阻塞
runtime_SemacquireMutex(&rw.writerSem, false, 0)
}
}
- 首先调用
rw.w.Lock()
获取互斥锁,确保在修改readerCount
和readerWait
时的原子性。 - 调用
atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders)
将readerCount
减去一个特定的常量rwmutexMaxReaders
,使其变为负数,表示有写操作正在请求锁。然后加上rwmutexMaxReaders
得到当前正在进行的读操作数量r
。 - 如果
r
不为 0,说明有读操作正在进行,将r
赋值给readerWait
,表示写操作需要等待这些读操作完成。如果readerWait
不为 0,当前写操作会调用runtime_SemacquireMutex
方法,通过writerSem
信号量进入阻塞状态,直到所有读操作完成。
释放读锁(RUnlock
方法)
func (rw *RWMutex) RUnlock() {
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
// 有写操作在等待,检查是否所有读操作都完成了
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
// 所有读操作都完成了,唤醒等待的写操作
runtime_Semrelease(&rw.writerSem, false, 1)
}
}
}
- 调用
atomic.AddInt32(&rw.readerCount, -1)
原子地将readerCount
减 1,表示一个读操作结束。 - 如果
readerCount
为负数,说明有写操作在等待。此时将readerWait
减 1,如果readerWait
变为 0,说明所有读操作都完成了,调用runtime_Semrelease
方法,通过writerSem
信号量唤醒等待的写操作。
释放写锁(Unlock
方法)
func (rw *RWMutex) Unlock() {
// 恢复读操作计数器
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
// 唤醒所有等待的读操作
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
// 释放互斥锁
rw.w.Unlock()
}
- 调用
atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
将readerCount
恢复为正数,表示写操作结束。 - 根据
readerCount
的值,循环调用runtime_Semrelease
方法,通过readerSem
信号量唤醒所有等待的读操作。 - 最后调用
rw.w.Unlock()
释放互斥锁,允许其他操作继续进行。
总结
Go 语言的 sync.RWMutex
读写锁通过使用互斥锁、信号量和计数器等机制,实现了对共享资源的读写分离控制。多个读操作可以并发进行,提高了系统的并发性能;而写操作具有独占性,保证了数据的一致性和完整性。在实际应用中,根据不同的场景合理使用读写锁,可以有效地提高程序的性能和并发处理能力。
说人话版:
核心规则和数据
把读写锁想象成图书馆的管理规则,而图书馆里的书就是共享资源。读写锁有几个重要的 “规则记录本”(对应代码里的数据):
- 普通门锁钥匙(
w
):这就像是一把特殊的钥匙,只有拿到它才能去修改 “规则记录本” 上的内容,保证记录信息的准确性。 - 写操作等待室门铃(
writerSem
):当有读者想要修改图书内容(写操作),但前面有其他读者正在看书(读操作)时,他就得去等待室,等待室的门铃响了才代表可以进去修改图书。 - 读操作等待室门铃(
readerSem
):要是有读者想进图书馆看书(读操作),但刚好有人在修改图书(写操作),他就得去另一个等待室,等门铃响了才能进去看书。 - 正在看书的读者数量(
readerCount
):记录当前有多少读者正在图书馆里看书。 - 修改图书的人要等的看书读者数量(
readerWait
):当有读者想要修改图书时,要先看看有多少人正在看书,把这个数量记下来,等这些人都看完书才能去修改。
获取读锁(读者想进图书馆看书)
- 图书馆门口有个计数器,代表正在看书的读者数量。有新读者想进来时,计数器加 1。
- 如果发现计数器变成负数了(这就好像图书馆挂出了 “有人正在修改图书,暂时不能进” 的牌子),那新读者就得去读操作等待室等着,等门铃响了才能进去。
获取写锁(读者想修改图书内容)
- 想修改图书的读者得先拿到普通门锁钥匙,这样才能去改 “规则记录本” 上的信息。
- 他会先看一下当前有多少读者正在看书,把这个数量记下来当作要等的人数。同时,他会把计数器改成负数,告诉后面来的读者 “有人要修改图书啦,先别进”。
- 如果还有读者正在看书,那想修改图书的读者就得去写操作等待室等着,等门铃响了才能去修改。
释放读锁(读者看完书离开图书馆)
- 读者看完书离开时,计数器减 1。
- 如果发现计数器是负数(说明有人在等着修改图书),那就看一下要等的看书读者数量,如果这个数量变成 0 了,就按一下写操作等待室的门铃,告诉等着修改图书的人可以进来了。
释放写锁(读者修改完图书)
- 读者修改完图书后,把计数器恢复成正常的正数,代表可以有新读者进来看书了。
- 根据计数器上记录的之前因为修改图书而在等待的读者数量,挨个按读操作等待室的门铃,让他们进来看书。
- 最后把普通门锁钥匙还回去,其他人又可以来修改 “规则记录本” 了。
通过这样的方式,读写锁实现了多个读者可以同时看书(多个读操作并发),但有人修改图书时其他人不能看也不能改(写操作独占),提高了图书馆(程序)的使用效率。
...