互斥锁mutex
加解锁
自旋加锁模式 基本使用 读写锁rwmutex
接口互斥
写阻塞读读阻塞写避免饿死
锁是为了避免竞争而建立的并发控制手段,为有序地访问共享资源。
Mutex为一结构体类型,对外暴露Lock与Unlock接口。加锁与解锁要成对出现(应加锁后,立即用defer解锁),重复解锁会引起panic。
Mutex内存布局:
Mutex有以下状态:
Locked:是否已被锁定(0:没锁定,1:锁定);Woken:是否有协程已被唤醒,正处于加锁状态(0:无协程唤醒,1:有协程唤醒);Starving:是否处于饥饿状态(0:没有饥饿,1:饥饿状态);Waiter:阻塞的等待锁的协程数(解锁时据此判断是否要释放信号量); 加解锁
协程间抢锁实际上是抢给Locked赋值的权利(能给Locked域置1,说明抢锁成功);抢不到的阻塞等待Mutex.sema信号量。
Woken状态用于加锁和解锁过程的通信;处于自旋状态的加锁协程会把Woken标记为1,通知解锁协程不必释放信号量了。
加锁、唤醒示意图(最基本的情形):
正常模式下,被阻塞的协程会进入等待队列;当持有锁协程释放锁时,会释放唤醒信号来唤醒等待的协程。
自旋自旋对应于CPU的‘PAUSE’指令(CPU空转),不同于sleep,其不需要把协程转为睡眠状态;加锁时程序会自动判断是否可自旋,自旋必须满足(要不忙):
自旋次数要足够小(通常最多不超4次);CPU核数要大于1(否则自旋无意义);协程调度机制中的Process数量要大于1;协程调度机制中可运行队列必须为空;
自旋优势是更充分利用CPU,尽量避免协程切换。若自旋过程中获得锁,那么之前被阻塞协程将无法获得锁,从而可能会进入饥饿状态;为避免协程长时间无法获取锁,自1.8版本后,Mutex增加了Starving状态,此状态下不会自旋(释放锁时,一定会唤醒一个协程并让其成功加锁)。
加锁模式每个Mutex都有两个模式,称为Normal和Starving:
Normal模式:若加锁不成功,不会立即转入阻塞队列,而是判断是否满足自旋条件;Starvation模式:处于饥饿模式下,不会启动自旋过程;一旦有协程释放了锁,那么一定会唤醒协程,被唤醒的协程将会成功获取锁,同时也会把等待计数减1。
释放锁时如果发现有阻塞等待的协程,还会释放一个信号量来唤醒一个等待协程,被唤醒的协程得到CPU后开始运行,此时发现锁已被抢占了,自己只好再次阻塞,不过阻塞前会判断自上次阻塞到本次阻塞经过了多长时间,如果超过1ms的话,会将Mutex标记为”饥饿”模式,然后再阻塞。
基本使用sync包中提供了锁相关的一系列同步原语,用于加解锁:
import ( "fmt" "sync" ) func ShowMutex() { var syn sync.Mutex var count = 0 var wg sync.WaitGroup wg.Add(10) for i := 0; i < 10; i++ { go func() { defer wg.Done() for j := 0; j < 100000; j++ { syn.Lock() count++ syn.Unlock() } }() } wg.Wait() fmt.Println(count) }读写锁rwmutex
读写互斥锁,可增加并发能力(程序中一般是读多、写少):
写锁阻塞其他写锁;写锁阻塞读锁;读锁阻塞写锁;读锁不阻塞读锁; 接口
RWMutex提供了四个接口:
RLock(读锁定):增加读 *** 作计数,(若有写 *** 作)阻塞等待写 *** 作结束;RUnlock(读解锁):减少读 *** 作计数,(最后一个读 *** 作、且有写锁定)唤醒等待等待写 *** 作的协程;Lock(写锁定):获取互斥锁,(若有读 *** 作)等待所有读 *** 作结束;Unlock(写解锁):唤醒因读锁定而被阻塞的协程(若有),解除互斥锁;
读写锁定义:
type RWMutex struct { w Mutex //用于控制多个写锁, 获得写锁首先要获取该锁, 如果有一个写锁在进行, 那么再到来的写锁将会阻塞于此 writerSem uint32 //写阻塞等待的信号量, 最后一个读者释放锁时会释放信号量 readerSem uint32 //读阻塞的协程等待的信号量, 持有写锁的协程释放锁后会释放信号量 readerCount int32 //记录读者个数 readerWait int32 //记录写阻塞时读者个数 }互斥 写阻塞读
写 *** 作是如何阻止读 *** 作的:
readerCount是个整型值,用于表示读者数量,不考虑写 *** 作的情况下,每次读锁定将该值+1,每次解除读锁定将该值-1,所以readerCount取值为[0,N](N为读者个数,最大可支持 2 30 2^{30} 230个并发读者)。当写锁定进行时,会先将readerCount减去 2 30 2^{30} 230,从而readerCount变成了负值,此时再有读锁定到来时检测到readerCount为负值,便知道有写 *** 作在进行,只好阻塞等待。真实的读 *** 作个数并不会丢失,只需要将readerCount加上 2 30 2^{30} 230即可获得。 读阻塞写
读 *** 作是如何阻止写 *** 作的:
读锁定会先将readerCount加1,此时写 *** 作到来时发现读者数量不为0,会阻塞等待所有读 *** 作结束。 避免饿死
为什么写锁定不会被饿死:
写 *** 作到来时,会把readerCount值拷贝到readerWait中,用于标记排在写 *** 作前面的读者
个数。读 *** 作结束后,除了会递减readerCount,还会递减readerWait值,当readerWait值变为0时唤醒写 *** 作。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)