互斥锁Mutex
type Mutex struct {// 表示互斥锁状态state int32// 表示信号量,协程阻塞等待该信号量,解锁的协程释放信号量从而唤醒等待信号量的协程sema uint32
}
Locked: 表示该Mutex是否已被锁定,0:没有锁定 1:已被锁定
Woken: 表示是否有协程已被唤醒,0:没有协程唤醒 1:已有协程唤醒,正在加锁过程中
Starving:表示该Mutex是否处于饥饿状态,0:没有饥饿 1:饥饿状态,说明有协程阻塞了超过1ms
Waiter: 表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量
简单加锁
判断Locked标志位是否为0,如果是0则把Locked置1,代表加锁成功
加锁被阻塞
加锁前,如果Locked为1,则将Waiter+1,表示此协程阻塞等待,直到Locked为0后被唤醒
简单解锁
如果没有其他协程阻塞等待加锁,Waiter为0,则直接把Locked置0,
解锁并唤醒协程
如果Waiter大于0,有其他协程阻塞等待加锁,则当前协程解锁:将Locked置0;释放一个信号量,唤醒一个阻塞协程
自旋过程
自旋相当于CPU空转,sleep一小段时间。自旋过程当前协程会持续探测Locked是否改为0,如果改为0则可以直接运行,不必进入阻塞状态。可以充分利用CPU,避免协程切换
问题:如果加锁的协程特别多,每次都通过自旋获得锁,那么之前被阻塞的进程将很难获得锁,从而进入饥饿状态。1.8版本以来增加了一个状态,即Mutex的Starving状态。这个状态下不会自旋,一旦有协程释放锁,那么一定会唤醒一个协程并成功加锁
模式
normal模式
默认模式,该模式下,协程如果加锁不成功不会立即转入阻塞排队,而是判断是否满足自旋的条件,如果满足则会启动自旋过程,尝试抢锁
starvation模式
处于饥饿模式下,不会启动自旋过程,也即一旦有协程释放了锁,那么一定会唤醒协程,被唤醒的协程将会成功获取锁,同时也会把等待计数减1
读写锁RWMutex
读读不互斥,读写互斥,写写互斥
P操作 信号量值 -1,如果小于0就阻塞等待;V操作 信号量值 +1,唤醒阻塞的线程
读者和写者加锁时,进行P操作;读者和写者释放锁时,进行V操作
写者加锁和读者释放锁,操作writerSem;读者加锁和写者释放锁,操作readerSem
type RWMutex struct {// 控制写锁,获得写锁首先要获取该锁,实现了写写互斥w Mutex // 写阻塞等待的信号量,最后一个读者释放锁时会释放此信号量,通知写者进行写操作writerSem uint32 // 读阻塞等待的信号量,持有写锁的协程释放锁后会释放此信号量,通知读者进行读操作readerSem uint32 // 实现了读写互斥,读读不互斥// 只要有读操作到来,则此字段+1(读读不互斥)// 1.记录当前持有读锁(正在读或者等待写完再读)的协程数量 2.<0代表有写者在等(读写互斥)readerCount int32 // 记录写阻塞时需要等待多少个读者释放读锁(实现写操作不会被饿死)readerWait int32
}
源码
写者加锁Lock()
const rwmutexMaxReaders = 1 << 30func (rw *RWMutex) Lock() {// 先获取互斥锁rw.w.Lock()// 先将readerCount加一个很大的负数使其<0(表示当前有写者正在等或执行),然后用r记录原来的readerCountr := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders// r!=0说明当前还有读者正在读,随后让readerWait加上r(readerCount),表示要等待的读者完成的个数if r != 0 && rw.readerWait.Add(r) != 0 {// runtime_SemacquireRWMutex()阻塞当前goroutine,直到某个条件满足释放信号量的操作// 当前写者需要阻塞(writerSem信号量为0则阻塞),直到所有读者释放锁(信号量大于0则执行)runtime_SemacquireRWMutex(&rw.writerSem, false, 0)}
}
先获取写锁
将readerCount加一个很大的负数使其<0,表示当前有写者正在等待或执行
将readerWait加上原本的readerCount,表示当前写操作之前要等待的读操作数量
如果readerCount和readerWait都不为0,则等最后一个读操作执行完毕释放writerSem信号量,再执行写操作
读者加锁RLock()
func (rw *RWMutex) RLock() {// 读者数量+1(先加上读锁),然后判断如果小于0表示有写者持有写锁,则不能读者立即加锁if rw.readerCount.Add(1) < 0 {// 读者等待写者信号量释放runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)}
}
先readerCount+1,将读操作入队排队
等写操作执行完(释放readerSem信号量),再执行读操作
写者释放锁UnLock()
func (rw *RWMutex) Unlock() {// 在Lock加锁的时候对readerCount减去了rwmutexMaxReaders,这时加回来还原恢复r := rw.readerCount.Add(rwmutexMaxReaders)// 加rwmutexMaxReaders之前,readerCount减去了rwmutexMaxReaders// 如果这个readerCount>=0,说明没有写者if r >= rwmutexMaxReaders {race.Enable()fatal("sync: Unlock of unlocked RWMutex")}// 当前写锁期间累积了多少个阻塞的读者(readerCount),就释放几次readerSemfor i := 0; i < int(r); i++ {runtime_Semrelease(&rw.readerSem, false, 0)}// 释放写锁rw.w.Unlock()
}
恢复readerCount为原本的值
有多少个readerCount(写操作期间,后来的读操作被阻塞等待,读操作数量),就释放几次readerSem信号量,将所有等待的读操作全部唤醒
释放写锁
读者释放锁RUnLock()
func (rw *RWMutex) RUnlock() {// 先将readerCount减1,释放读锁。如果readerCount<0,表示有写者在等,则进入rUnlockSlow()if r := rw.readerCount.Add(-1); r < 0 {rw.rUnlockSlow(r)}
}func (rw *RWMutex) rUnlockSlow(r int32) {// 边界问题处理// r+1 ==0 表示没有读者加锁,却调用了释放读锁// r+1 == -rwmutexMaxReaders表示在没有读者加锁,有写者持有互斥锁的情况下,却释放了读锁if r+1 == 0 || r+1 == -rwmutexMaxReaders {race.Enable()fatal("sync: RUnlock of unlocked RWMutex")}// readerWait-1,将写者需要等待的读者-1// 如果readerWait-1后为0,表示这是最后一个读者,要发送信号量通知写者if rw.readerWait.Add(-1) == 0 {runtime_Semrelease(&rw.writerSem, false, 1)}
}
先释放读锁(readerCount-1)
如果有写者在等(readerCount<0),并且当此读者为写操作需要等待的最后一个读者时(readerWait-1==0),释放writerSem信号量通知写者进行写操作
写操作如何阻止读操作(正在写操作,则不能读)
每次读锁定将readerCount值+1,每次解除读锁定将该值-1
写锁定进行时,会先将readerCount减去固定的大数,从而readerCount变成负值,此时再有读锁定到来时检测到readerCount为负值,便知道有写操作在进行,只好阻塞等待。而真实的读操作个数并不会丢失,只需要将readerCount加上固定大数即可获得
所以,写操作将readerCount变成负值来阻止读操作的
读操作如何阻止写操作(正在读操作,则不能写)
读锁定会先将readerCount加1,此时写操作到来时发现读者数量readerCount不为0,会阻塞等待所有读操作结束
所以,读操作通过readerCount来将来阻止写操作的
为什么写锁定不会被饿死(写优先)
写操作要等待读操作结束后才可以获得锁,写操作等待期间可能还有新的读操作持续到来,如果写操作等待所有读操作结束,很可能被饿死
readerWait标记写操作到来时,前面正在执行的读操作的个数,等这些读操作完毕后readerWait减为0,通知写操作执行(由于只要有读操作到来,都会将readerCount+1,所以无法知道读操作的先后顺序,这里用readerWait临时记录写操作到来时前面的读操作个数,实现了给写操作“排队”的效果,使写操作不会被后续读操作“插队”)
写操作到来时,会把readerCount值拷贝到readerWait中,用于标记排在写操作前面的读者个数
写操作前面的读操作结束后,除了会递减readerCount,还会递减RWMutex.readerWait,当readerWait值变为0时(写操作前面的读操作都已结束),唤醒写操作
2. 同时写操作一旦到来,就会将readerCount改为负值,表示有写操作在等待,使得写操作不会被后续到来的读操作抢占