GO 读写锁源码剖析

11-27 5,336 views

        今天来说说Go读写锁的实现及源码剖析。在Go语言中读写锁用RWMutex表示。并发编程中使用读写锁,如果没有写操作,多个并发读操作之间并没有任何竞争关系。当然,除了对读计数器的原子操作。如果有写操作,任何写操作都会占有锁,但写操作会等到所有已经开始读的操作完成之后,写操作完成后优先唤醒读操作,然后才会释放写操作锁。

type RWMutex struct {
        w           Mutex  // 写锁
        writerSem   uint32 // 写信号量
        readerSem   uint32 // 读信号量
        readerCount int32  // 读的总量
        readerWait  int32  // 等待读的数量
}

        Mutex w是用来阻塞多个写操作的,读操作并不需要处理该锁;writeSem和readerSem是读、写事务的信号量。获取到该信号,就可以对该事务进行处理;readerCount是读计数器总数;readerWait是一个计数器,用来表示在写操作之前有多少个读操作正在进行。

func (rw *RWMutex) RLock() {
        // 如果累加读计数的结果依然是负数,表明写操作正在进行
        // 计数累计依然有效,因为低阈值是固定的
        if atomic.AddInt32(&rw.readerCount, 1) < 0 {
                // 目前写操作正在执行
                // 读操作休眠,等待唤醒
                runtime_Semacquire(&rw.readerSem)
        }
}
func (rw *RWMutex) RUnlock() {
        // 如果写操作正在等待
        if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
                if r+1 == 0 || r+1 == -rwmutexMaxReaders {
                        panic("sync: RUnlock of unlocked RWMutex")
                }
                
                // 递减readerWait计数,直到归零时释放信号唤醒写操作
                if atomic.AddInt32(&rw.readerWait, -1) == 0 {
                        runtime_Semrelease(&rw.writerSem)
                }
        }
}

        每次RLock都会累加readerCounter,RUnlock则递减。以此来统计当前有多少正在进行读操作。在RLock处理读事务之前,进行了原子递加操作,然后比较该值是否小于0。当小于0时,则表示写操作正在进行。此时虽然计数器加1,但该读事务会进入休眠状态,等待写操作完成后,被唤醒才能进行读操作。在RUnlock时也进行了原子递减操作,然后判断该值是否小于0,如果小于0则表示当前有写操作,然后将readerWait计数器递减,直到为0。然后进入写操作。readWait计数器上面已经说过了,该计数器是在写操作之前已经获取到RLock的事务。

func (rw *RWMutex) Lock() {
        // 获取写独占锁,阻塞其它写操作
        rw.w.Lock()
        
        // 将读操作计数递减到一个很低的阈值,并返回原计数值
        r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
        
        // 如果依然有读操作,那么休眠等待读操作完成
        // 未完成读操作计数写入readerWait
        if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
        	// 写操作进入休眠状态,等待被唤醒
                runtime_Semacquire(&rw.writerSem)
        }
}

func (rw *RWMutex) Unlock() {

        // 恢复readerCount计数,这其中包括因等待写操作完成累加的计数
        r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
        if r >= rwmutexMaxReaders {
                panic("sync: Unlock of unlocked RWMutex")
        }
        
        // 根据readerCounter,发送多个信号通知,唤醒所有休眠的读操作
        for i := 0; i < int(r); i++ {
                runtime_Semrelease(&rw.readerSem)
        }

        // 解除写操作独占
        rw.w.Unlock()
}

         当写操作触发Lock时,首先获取w锁。然后计算有多少未完成的读操作,将结果写入readerWait,然后休眠等待所有读操作完成。当读操作RUnlock发现写操作等待时,会递减readerWait。当最后一个归零时,唤醒写操作。同理,在写操作未完成时,Rlock也会陷入休眠状态,直到Unlock结束唤醒。读写锁是不是更像是CSP模式说的那种程序的编排,谁获取到信号则执行事务,否则进入休眠。

const rwmutexMaxReaders = 1 << 30

         写操作标志就是将readerCounter减去一个rwmutexMaxReaders阈值,这会将其设置为一个极低负值。读操作依然可以累加或者降低readerCounter,但因为这个负值过大,结果依然是负值,从而指导写操作正在等待。当然,计数加减操作结果并不影响readerCounter计数,因为只要再次加上该阈值,就可以恢复工作。这是不是一个很巧妙的设计。

        最近剖析了一些Go标准库的源码,发现其中有很多巧妙的设计。这些可能自己之前没有想到过,但是如果今后真的有一天自己也需要设计这样一个系统,还是可以借鉴的。哈哈~