在 Go 语言中,sync.RWMutex 读写锁的实现原理涉及多个关键部分,下面从其核心数据结构、获取读锁、获取写锁、释放读锁和释放写锁等方面详细介绍:

核心数据结构

type RWMutex struct {
    w           Mutex  // 用于保护写操作的互斥锁
    writerSem   uint32 // 写操作的信号量
    readerSem   uint32 // 读操作的信号量
    readerCount int32  // 当前读操作的数量
    readerWait  int32  // 写操作等待时需要等待的读操作数量
}
  • w:这是一个普通的互斥锁(Mutex),用于保护对 readerCountreaderWait 等字段的并发访问,确保在修改这些关键状态时的原子性。
  • 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() 获取互斥锁,确保在修改 readerCountreaderWait 时的原子性。
  • 调用 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。
  2. 如果发现计数器变成负数了(这就好像图书馆挂出了 “有人正在修改图书,暂时不能进” 的牌子),那新读者就得去读操作等待室等着,等门铃响了才能进去。

获取写锁(读者想修改图书内容)

  1. 想修改图书的读者得先拿到普通门锁钥匙,这样才能去改 “规则记录本” 上的信息。
  2. 他会先看一下当前有多少读者正在看书,把这个数量记下来当作要等的人数。同时,他会把计数器改成负数,告诉后面来的读者 “有人要修改图书啦,先别进”。
  3. 如果还有读者正在看书,那想修改图书的读者就得去写操作等待室等着,等门铃响了才能去修改。

释放读锁(读者看完书离开图书馆)

  1. 读者看完书离开时,计数器减 1。
  2. 如果发现计数器是负数(说明有人在等着修改图书),那就看一下要等的看书读者数量,如果这个数量变成 0 了,就按一下写操作等待室的门铃,告诉等着修改图书的人可以进来了。

释放写锁(读者修改完图书)

  1. 读者修改完图书后,把计数器恢复成正常的正数,代表可以有新读者进来看书了。
  2. 根据计数器上记录的之前因为修改图书而在等待的读者数量,挨个按读操作等待室的门铃,让他们进来看书。
  3. 最后把普通门锁钥匙还回去,其他人又可以来修改 “规则记录本” 了。

通过这样的方式,读写锁实现了多个读者可以同时看书(多个读操作并发),但有人修改图书时其他人不能看也不能改(写操作独占),提高了图书馆(程序)的使用效率。