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 |