IT评测·应用市场-qidao123.com
标题:
golang单机锁实现
[打印本页]
作者:
用多少眼泪才能让你相信
时间:
6 天前
标题:
golang单机锁实现
1、锁的概念引入
起首,为什么必要锁?
在并发编程中,多个线程或历程可能
同时访问和修改同一个共享资源
(比方变量、数据结构、文件)等,若不引入合适的同步机制,会引发以下问题:
数据竞争
:多个线程同时修改一个资源,最终的结果跟线程的实行次序有关,结果是不可猜测的。
数据不一致
:一个线程在修改资源,而另一个线程读取了未修改完的数据,从而导致读取了错误的数据。
资源竞争
:多线程竞争同一个资源,浪费系统的性能。
因此,我们必要一把锁,来保证同一时间只有一个人能写数据,确保共享资源在并发访问下的正确性和一致性。
在这里,引入两种常见的并发控制处理机制,即
乐观锁
与
悲观锁
:
乐观锁:假定在并发操作中,资源的抢占并不是很猛烈,数据被修改的可能性不是很大,那这时候就不必要对共享资源区进行加锁再操作,而是
先修改了数据,最终来判定数据有没有被修改
,没有被修改则提交修改指,否则重试。
悲观锁:与乐观锁相反,它假设场景的资源竞争猛烈,对共享资源区的访问必须要求持有锁。
针对差别的场景必要采取因地制宜的策略,比较乐观锁与悲观所,它们的优缺点显而易见:
策略优点缺点乐观锁不必要实际上锁,性能高若冲突时,必要重新进行操作,多次重试可能会导致性能降落明显悲观锁访问数据一定必要持有锁,保证并发场景下的数据正确性加锁期间,其他等候锁的线程必要被阻塞,性能低
2、Sync.Mutex
Go对单机锁的实现,考虑了实际情况中协程对资源竞争程度的变革,制定了一套
锁升级
的过程。具体方案如下:
起首采取乐观的态度,Goroutine会保持
自旋态
,通过
CAS
操作尝试获取锁。
当多次获取失败,将会由乐观态度转入悲观态度,判定当前并发资源竞争程度剧烈,进入阻塞态等候被唤醒。
从
乐观转向悲观
的判定规则如下,满足其中之一即发生转变:
Goroutine自旋尝试
次数超过4次
当前P的实行队列中存在等候被实行的G(避免自旋影响GMP调度性能)
CPU是单核的(其他Goroutine实行不了,自旋无意义)
除此之外,为了防止被阻塞的协程等候过长时间也没有获取到锁,导致用户的整体体验降落,引入了
饥饿
的概念:
饥饿态
:若Goroutine被阻塞等候的时间>1ms,则这个协程被视为处于饥饿状态
饥饿模式
:表现当前锁是否处于特定的模式,在该模式下,锁的交接是
公平
的,按次序交给等候最久的协程。
饥饿模式与正常模式的
转变规则
如下:
普通模式->饥饿模式:存在阻塞的协程,
阻塞时间超过1ms
饥饿模式->普通模式:阻塞队列清空,亦大概获得锁的协程的等候时间小于1ms,则恢复
接下来步入源码,观看具体的实现。
2.1、数据结构
位于包sync/mutex.go中,对锁的定义如下:
type Mutex struct {
state int32
sema uint32
}
复制代码
state:标识如今锁的状态信息,包括了是否处于饥饿模式、是否存在唤醒的阻塞协程、是否上锁、以及处于等候锁的协程个数有多少。
seme:用于阻塞和唤醒协程的信号量。
将state看作一个二进制字符串,它存储信息的规则如下:
第一位标识是否处于上锁,0表现否,1表现上锁(mutexLocked)
第二位标识是否存在唤醒的阻塞协程(mutexWoken)
第三位标识是否处于饥饿模式(mutexStarving)
从第四位开始,记录了处于阻塞态的协程个数
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
starvationThresholdNs = 1e6 //饥饿阈值
)
复制代码
(2)进入尝试获取锁的循环中,两个if表现:
<ul>若锁
处于上锁状态
,并且不
处于饥饿状态中
,并且当前的协程
允许继续自旋下去
(非单核CPU、自旋次数>mutexWaiterShift == 0 { throw("sync: inconsistent mutex state") } //将要更新的信号量 delta := int32(mutexLocked - 1mutexWaiterShift == 1 { delta -= mutexStarving } atomic.AddInt32(&m.state, delta) break } awoke = true iter = 0 //.... } else { //... }[/code]从阻塞中唤醒,起首计算一些协程的阻塞时间,以及当前的最新锁状态。
若
锁处于饥饿模式
:那么当前协程将直接获取锁,当前协程是因为饥饿模式被唤醒的,不存在其他协程抢占锁。于是更新信号量,将记录阻塞协程数-1,将锁的上锁态置1。若当前从饥饿模式唤醒的协程,
等候时间已经不到1ms了大概是最后一个等候的协程
,那么将将锁从饥饿模式转化为正常模式。至此,获取成功,退出函数。
否则,只是普通的随机唤醒,于是开始尝试进行抢占,回到步骤1。
2.4、释放锁Unlock()
func (m *Mutex) Lock() {
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
m.lockSlow()
}
复制代码
通过原子操作,直接将锁的mutexLocked标识置为0。若置0后,锁的状态不为0,那就说明存在必要获取锁的协程,步入unlockSlow。
2.5、unlockSlow()
[code]func (m *Mutex) unlockSlow(new int32) { if (new+mutexLocked)&mutexLocked == 0 { fatal("sync: unlock of unlocked mutex") } if new&mutexStarving == 0 { old := new for { if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 { return } new = (old - 1
欢迎光临 IT评测·应用市场-qidao123.com (https://dis.qidao123.com/)
Powered by Discuz! X3.4