✍个人博客:Pandaconda-CSDN博客
📣专栏地址:http://t.csdnimg.cn/UWz06
📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力
4. R WMutex 实现
sync.RWMutex
是 Go 语言标准库提供的一种读写锁,用于在多个 goroutine 同时访问共享资源时进行保护。与 sync.Mutex
类似,sync.RWMutex
也是通过互斥锁实现的,但是它允许多个 goroutine 同时获取读锁,而只允许一个 goroutine 获取写锁。
下面是一个简单的 sync.RWMutex
的实现示例:
type RWMutex struct { writerSem chan struct{} // 写者用的信号量 readerSem chan struct{} // 读者用的信号量 readerCount int // 当前持有读锁的goroutine数量 writerCount int // 当前持有写锁的goroutine数量 readerWait int // 正在等待读锁的goroutine数量 writerWait int // 正在等待写锁的goroutine数量 writerLocked bool // 是否有goroutine持有写锁 } func NewRWMutex() *RWMutex { return &RWMutex{ writerSem: make(chan struct{}, 1), readerSem: make(chan struct{}, 1), } } // 获取读锁 func (m *RWMutex) RLock() { // 获取读锁的过程需要加锁 m.writerSem <- struct{}{} // 防止写者获取锁 m.readerSem <- struct{}{} // 获取读锁的信号量 // 更新状态 m.readerCount++ if m.writerLocked || m.writerWait > 0 { m.readerWait++ <-m.readerSem // 等待写者释放锁 m.readerWait-- } // 释放加锁时获取的信号量 <-m.writerSem } // 释放读锁 func (m *RWMutex) RUnlock() { // 获取读锁的过程需要加锁 m.writerSem <- struct{}{} // 防止写者获取锁 // 更新状态 m.readerCount-- if m.readerCount == 0 && m.writerWait > 0 { <-m.writerSem // 优先唤醒写者 } // 释放加锁时获取的信号量 <-m.writerSem } // 获取写锁 func (m *RWMutex) Lock() { // 获取写锁的过程需要加锁 m.writerSem <- struct{}{} // 防止其他写者获取锁 m.writerWait++ // 等待其他goroutine释放读锁或写锁 for m.writerLocked || m.readerCount > 0 { <-m.readerSem } // 更新状态 m.writerWait-- m.writerLocked = true // 释放加锁时获取的信号量 <-m.writerSem } // 释放写锁 func (m *RWMutex) Unlock() { // 获取写锁的过程需要加锁 m.writerSem <- struct{}{} // 更新状态 m.writerLocked = false if m.writerWait > 0 { <-m.writerSem } else if m.readerWait > 0 { for i := 0; i < m.readerCount; i++ { m.readerSem <- struct{}{} // 优先唤醒读者 } } // 释放加锁时获取的信号量 <-m.writerSem }
在这个实现中,sync.RWMutex
包含以下成员:
writerSem
和readerSem
:两个用于同步的信号量通道。写锁会在writerSem
上等待,读锁会在readerSem
上等待。readerCount
和writerCount
:当前持有读锁和写锁的 goroutine 数量。readerWait
和writerWait
:正在等待读锁和写锁的 goroutine 数量。writerLocked
:标记当前是否有 goroutine 持有写锁。
在读锁和写锁获取和释放的过程中,都需要先获取 writerSem
信号量防止其他写者获取锁。获取读锁时还需要获取 readerSem
信号量,而获取写锁时需要等待其他 goroutine 释放读锁或写锁。
这个实现中有两个重要的细节:
优先唤醒写者:在释放读锁或写锁时,如果有正在等待的写锁 goroutine,应该优先唤醒它们,因为写锁的优先级更高。
读锁的等待问题:在等待读锁的 goroutine 中,如果有其他 goroutine 正在持有写锁或等待写锁,那么这些读锁 goroutine 应该等待写锁 goroutine 释放锁,避免因等待读锁而导致写锁饥饿。
5. R WMutex 注意事项
RWMutex 是单写多读锁,该锁可以加多个读锁或者一个写锁。
读锁占用的情况下会阻止写,不会阻止读,多个 Goroutine 可以同时获取读锁。
写锁会阻止其他 Goroutine(无论读和写)进来,整个锁由该 Goroutine 独占。
适用于读多写少的场景。
RWMutex 类型变量的零值是一个未锁定状态的互斥锁。
RWMutex 在首次被使用之后就不能再被拷贝。
RWMutex 的读锁或写锁在未锁定状态,解锁操作都会引发 panic。
RWMutex 的一个写锁去锁定临界区的共享资源,如果临界区的共享资源已被(读锁或写锁)锁定,这个写锁操作的 goroutine 将被阻塞直到解锁。
RWMutex 的读锁不要用于递归调用,比较容易产生死锁。
RWMutex 的锁定状态与特定的 goroutine 没有关联。一个 goroutine 可以 RLock(Lock),另一个 goroutine 可以 RUnlock(Unlock)。
写锁被解锁后,所有因操作锁定读锁而被阻塞的 goroutine 会被唤醒,并都可以成功锁定读锁。
读锁被解锁后,在没有被其他读锁锁定的前提下,所有因操作锁定写锁而被阻塞的 Goroutine,其中等待时间最长的一个 Goroutine 会被唤醒。
6. Go 读写锁的实现原理?
概念:
读写互斥锁 RWMutex,是对 Mutex 的一个扩展,当一个 goroutine 获得了读锁后,其他 goroutine 可以获取读锁,但不能获取写锁;当一个 goroutine 获得了写锁后,其他 goroutine 既不能获取读锁也不能获取写锁(只能存在一个写者或多个读者,可以同时读)。
使用场景:
读多于写的情况(既保证线程安全,又保证性能不太差)。
底层实现结构:
互斥锁对应的是底层结构是 sync.RWMutex 结构体,,位于 src/sync/rwmutex.go 中
type RWMutex struct { w Mutex // 复用互斥锁 writerSem uint32 // 信号量,用于写等待读 readerSem uint32 // 信号量,用于读等待写 readerCount int32 // 当前执行读的 goroutine 数量 readerWait int32 // 被阻塞的准备读的 goroutine 的数量 }
操作:
读锁的加锁与释放
func (rw *RWMutex) RLock() // 加读锁 func (rw *RWMutex) RUnlock() // 释放读锁
加读锁
func (rw *RWMutex) RLock() { // 为什么readerCount会小于0呢?往下看发现writer的Lock()会对readerCount做减法操作(原子操作) if atomic.AddInt32(&rw.readerCount, 1) < 0 { // A writer is pending, wait for it. runtime_Semacquire(&rw.readerSem) } }
atomic.AddInt32(&rw.readerCount, 1)
调用这个原子方法,对当前在读的数量加 1,如果返回负数,那么说明当前有其他写锁,这时候就调用 runtime_SemacquireMutex
休眠当前 goroutine 等待被唤醒。
释放读锁
解锁的时候对正在读的操作减 1,如果返回值小于 0 那么说明当前有在写的操作,这个时候调用 rUnlockSlow
进入慢速通道。
func (rw *RWMutex) RUnlock() { if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 { rw.rUnlockSlow(r) } }
被阻塞的准备读的 goroutine 的数量减 1,readerWait 为 0,就表示当前没有正在准备读的 goroutine 这时候调用 runtime_Semrelease
唤醒写操作。
func (rw *RWMutex) rUnlockSlow(r int32) { // A writer is pending. if atomic.AddInt32(&rw.readerWait, -1) == 0 { // The last reader unblocks the writer. runtime_Semrelease(&rw.writerSem, false, 1) } }
写锁的加锁与释放。
func (rw *RWMutex) Lock() // 加写锁 func (rw *RWMutex) Unlock() // 释放写锁
加写锁
const rwmutexMaxReaders = 1 << 30 func (rw *RWMutex) Lock() { // First, resolve competition with other writers. 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_Semacquire(&rw.writerSem) } }
首先调用互斥锁的 lock,获取到互斥锁之后,如果计算之后当前仍然有其他 goroutine 持有读锁,那么就调用 runtime_SemacquireMutex
休眠当前的 goroutine 等待所有的读操作完成
这里readerCount 原子性加上一个很大的负数,是防止后面的协程能拿到读锁,阻塞读
释放写锁
func (rw *RWMutex) Unlock() { // Announce to readers there is no active writer. r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders) // Unblock blocked readers, if any. for i := 0; i < int(r); i++ { runtime_Semrelease(&rw.readerSem, false) } // Allow other writers to proceed. rw.w.Unlock() }
解锁的操作,会先调用 atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
将恢复之前写入的负数,然后根据当前有多少个读操作在等待,循环唤醒
注意点:
读锁或写锁在 Lock() 之前使用 Unlock() 会导致 panic 异常。
使用 Lock() 加锁后,再次 Lock() 会导致死锁(不支持重入),需 Unlock() 解锁后才能再加锁。
锁定状态与 goroutine 没有关联,一个 goroutine 可以 RLock(Lock),另一个 goroutine 可以 RUnlock(Unlock)。
互斥锁和读写锁的区别:
读写锁区分读者和写者,而互斥锁不区分。
互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。