golang之互斥锁mutex与读写锁

golang之互斥锁mutex与读写锁,第1张

golang之互斥锁mutex与读写锁

文章目录

互斥锁mutex

加解锁

自旋加锁模式 基本使用 读写锁rwmutex

接口互斥

阻塞读读阻塞写避免饿死
锁是为了避免竞争而建立的并发控制手段,为有序地访问共享资源。

互斥锁mutex

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时唤醒写 *** 作。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5712630.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存