背景
最近项目中遇到两次RWMutex死锁问题,所以稍微看了一下资料和源码,稍作记录
源码
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 int32 // number of pending readers readerWait int32 // number of departing readers }
RWMutex源码分析方面参考资料《go中的sync.RWMutex源码解读》写的比较详细和清晰易懂。
总结
go中锁都是不可重入的,所以同一个协程中获取两次RWmutex锁都可能出现死锁
1. 同一协程获取两次RLock
此场景参考前一篇文章《golang RWMutex RLock重入导致死锁》,同一协程两次获取RLock,如果在第二次获取RLock之前,有其他协程获取写锁Lock则会导致死锁。
这种场景和同一协程先获取Lock再获取RLock是一样的原理,由于协程1获取了RLock,导致协程2获取Lock时会被阻塞,然而协程2获取Lock时会将rw.readerCount置为<0的负值,然后协程1再获取RLock也会被阻塞,所以导致两个协程相互阻塞了。
2. 同一协程先获取RLock再获取Lock
func TestLockUp(t *testing.T) { var l sync.RWMutex l.RLock() t.Log("acquire read lock") l.Lock() t.Log("acquire write lock") l.Unlock() l.RUnlock() }
读锁是会阻塞写锁的;从源代码中可以看到,在获取Lock时,如果已经有协程获取了RLock,则获取Lock的协程会阻塞在获取rw.writerSem信号量上,在读锁RLock解锁时会唤醒该信号量,然后RLock在等待Lock解锁才能执行RUnLock,因此造成死锁。
3. 同一协程先获取Lock再获取RLock
func TestLockDown(t *testing.T) { var l sync.RWMutex l.Lock() t.Log("acquire write lock") l.RLock() t.Log("acquire read lock") l.RUnlock() l.Unlock() }
写锁也会阻止读锁;从源代码中可以看到,当一个协程获取写锁Lock时,会将rw.readerCount置为<0的负值,而当获取读锁RLock时,先对rw.readerCount加1,如果加1后的结果为负值,则表示有协程已经获取到写锁或者正在等待获取写锁,因而该获取读锁的协程会阻塞在获取rw.readerSem信号量上;因此同一个协程先后获取Lock和RLock会相互阻塞等待从而造成死锁。
从这里也能看出来,在RWMutex中,写锁的优先级高于读锁,只要有协程在等待获取写锁,后续的读锁都需要等待。
4. 同一协程获取两次Lock
func TestReLock(t *testing.T) { var l sync.RWMutex l.Lock() t.Log("acquire write lock") l.Lock() t.Log("acquire write lock") l.Unlock() l.Unlock() }
写锁阻止写锁;同样,第二个Lock会阻塞在获取rw.writeSem信号量上,导致两个Lock都无法执行Unlock。
总之一句话,go中RWLock不支持可重入,不要在同一协程调用多次RWMutex的锁,不管读锁还是写锁。
5. 写锁优先级高于读锁
有写锁等待时,优先加写锁,因此写锁不至于饿死一直无法获取到锁。
写操作到来时,会把RWMutex.readerCount
值拷贝到RWMutex.readerWait
中,用于标记排在写操作前面的读者个数。
前面的读操作结束后,除了会递减RWMutex.readerCount
,还会递减RWMutex.readerWait
值,当RWMutex.readerWait
值变为0时唤醒写操作。