Go RWMutex阅读

2021-08-25 | Tags: 源码

最近正好在读一些go的源码,有用到读写锁,看了一下标准实现,还是比较短的,就仔细看了看,看看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 int32  // number of pending readers
    readerWait  int32  // number of departing readers
}

总体来说比较简单writerSem,readerSem分别是读写唤醒的信号量,类似C++中的条件变量,主要为了最后一个读者或者写者Unlock时可以唤醒睡眠的进程

readerCount和readWait略微复杂一些,后面结合流程逻辑图解释一下

流程

读流程

graph TD A1[读上锁] A2[读上锁] A3[读上锁] A4[读解锁] B4(readerCount-=1,并检查是否正在写或正在等待写>>readCount是否小于0<<) BB(readerWait-=1,并检查readWait是否为0,即是否最后一个令写者等待的读者) B(readerCount+=1,并检查是否正在写或正在等待写>>readCount是否小于0<<) B1[等待写信号量]-->|写信号量release|B2 B2[读操作] A1-->B A2-->B A3-->B B-->|是|B1 B-->|否|B2 B2-->A4 A4-->B4 B4-->|是|BB BB-->|是|B5(release写信号量) B4-->|否|B6(写结束) BB-->|否|B6 B5-->B6

写流程

graph TD D[写上锁]-->E(Mutex竞争上锁--只有一个写进程可以继续) D1[写上锁]-->E D2[写上锁]-->E E-->F(r=readerCount,readerCount-=rwmutexMaxReaders>>令readerCount小于0<<,表示有写者进入) F-->F2(readerWait+=r) F2-->F1{r>0?} F1-->|是|G1(目前正有r个读者在读,等待读信号量release) F1-->|否|G2[写操作] G1-->|读信号量release|G2 G2-->H[写解锁] H-->H1(readerCount+=rwmutexMaxReaders>>去除写者标记<<,r=readerCount) H1-->H2(对所有的r,release读信号量) H2-->H3(写结束)

分析

对于readerCount来说,只有在读上锁和读解锁时进行+1和-1,表示的是当前正在读的读者的数量。同时又在写上锁和写解锁时通过减去和加上一个大值(1<<30,并判断readerCount是否小于0)来表示是否有写者进入。通过这种合并操作可以省下一个单独的变量,并将readerCount通过atomic进行原子操作,提升了运行效率。

对于readerWait来说,表示的是在写者进入进行上锁后,需要等待的正在读的读者完成读并读解锁的数量,表现在写上锁时readerWait+=r(readerCount),当readerWait>0时,写者需要等待r个读者的最后一个读者读解锁并唤醒写者。要注意的是,对于无写者进入的状态(readerCount>0),在读解锁的时候将不会操作readerWait。

对于整个读写锁的实现而言,与传统的读者和写者的公平竞争读写权限不太一样,写者由更高的权限。表现在一个写者上锁进入后,将挂起后续所有新的读者上锁的请求,并等待当前正在读的读者数量r,当所有r都读完成并解锁后,一定会优先让写者进行写操作。者种实现可以避免写操作一直等待的情况。

总体来说Go RWMutex的实现还是比较精巧的,其中通过尽量使用atomic原子操作提升了整体的运行效率,但是同时也增加了代码的理解难度,中间的一些竞争流程还是需要仔细的推演才能理解其正确性。