您好,登录后才能下订单哦!
本篇内容介绍了“Golang sync包之sync.Mutex怎么使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
type Mutex struct { state int32 sema uint32 }
我们可以发现sync.Mutex的数据结构十分简单, 他只有两个字段
state: 一个32位整数, 表示了当前锁的状态
sema: 他代表一个信号量(信号量是一个无符号整数,OS对信号量提供了能使其原子性自增/自减的PV操作, 可以通过信号量来实现互斥)
state的含义如图
他的低三位分别是Locked, Woken, Starving. 剩余28位表示在当前Mutex中阻塞的goroutine数目
Locked: 当前Mutex是否处于上锁状态, 0: 上锁, 1: 上锁.
Woken: 当前Mutex是否存在goroutine被唤醒, 0: 没有, 1: 存在被唤醒的goroutnine正在上锁.
Starving: 当Mutex处于何种模式, 0: 正常模式, 1: 饥饿模式
当我们声明一个变量但不给他赋值时, 他会被默认附上初始值, 这个初始值对于指针类型是nil, 而其他类型则是其零值.
因此, 当我们仅声明sync.Mutex时, 他会被默认赋值{state:0,sema:0}
, 而这个值恰好表示初始的锁状态. 所以 ,我们可以仅在声明后直接使用sync.Mutex.
在了解正常模式和饥饿模式前, 我们先来看下抢占式和非抢占式.
抢占式和非抢占式是调度的两种方式
抢占式: 当一个新goroutine请求锁时, 他会和当前被唤醒的goroutine进行竞争, 竞争成功就获取锁. 非抢占式: 如果阻塞队列中存在有其他goroutine, 那么新请求锁的goroutine会直接进入阻塞队列排队.
一般情况下, sync.Mutex处于正常模式, 在该模式下, 锁的获取方式为抢占式调度. 而一般情况下, 因为新请求锁的goroutine正在持有CPU且可能不止一个, 这就导致阻塞队列中新被唤醒的goroutine是难以抢占过新请求锁的goroutine的, 从而导致饥饿现象.
为了解决这种饥饿现象, go语言在go1.9的时候引入了饥饿模式, 在饥饿模式下, 锁的获取方式为非抢占式调度.
正常模式->饥饿模式:
当一个goroutine在阻塞队列等待超过1ms后, 他就会修改state使锁的状态变为饥饿模式
饥饿模式->正常模式:
当阻塞队列中某个goroutine阻塞时间小于1ms时
阻塞队列中重新变为空时
Lock方法有两种上锁方式, 快速上锁和慢速上锁
快速上锁
假如当前sync.Mutex的state=0, 那么就意味着当前sync.Mutex尚未被任何goroutine持有且阻塞队列中没有任何goroutine.
那么当前请求锁的goroutine就会通过CAS的方式修改state=1(意味着上锁), 然后返回.
慢速上锁
否则, 会调用lockSlow()方法来慢速上锁.
首先尝试通过自旋的方式获取锁, 在如下四个条件全部满足时会继续自旋, 如果成功通过自旋获取锁, 就直接返回
state不处于饥饿态
自旋次数小于4次
当前进程运行于多CPU主机
存在至少一个正在运行且工作队列为空的控制器P
之后会根据当前锁的不同状态做出不同的行为, 这里我们分类讨论.
正常状态
我们只截取少量的核心代码.
//如果处于正常态, 那么我们修改新状态为上锁 if old&mutexStarving == 0 { new |= mutexLocked } //通过cas的方式修改当前锁状态 if atomic.CompareAndSwapInt32(&m.state, old, new) { //如果旧状态处于正常态且未上锁, 那么就意味着当前线程抢占到了锁, 直接返回 if old&(mutexLocked|mutexStarving) == 0 { break //这里的break可以理解为return, 不用return是因为最后有个对race.Enabled的判断, 用于竞态检测 } ...... }
如果处于正常状态, 那么允许锁抢占, 我们先把新的锁状态修改为直接上锁, 这是因为假如他之前处于上锁态, 那么之后也会处于上锁态, 而如果处于未上锁状态, 那么当前goroutine会为其上锁, 因此锁的新状态一定处于上锁态.
然后我们通过CAS的方式用新状态替换旧状态. 替换成功后判断old(更新前的状态)是否处于正常态且未上锁, 如果是, 那么说明是当前goroutine上的锁, 这也就意味着当前goroutine成功获取锁了, 那么直接返回.
否则会调用runtime_SemacquireMutex
来在sema信号量下阻塞当前goroutine.
饥饿状态
饥饿状态会直接调用runtime_SemacquireMutex
来在sema信号量下阻塞当前goroutine.
被唤醒后
在某个goroutine被唤醒后, 他会判断自己阻塞时间是否超过1ms, 如果超过, 则切换为饥饿模式, 否则判断自己是否处于饥饿模式且阻塞的时间小于1ms, 如果处于饥饿模式且阻塞时间小于1ms, 那么就退出饥饿模式.
unlock()方法也分为快速解锁和慢速解锁两部分
快速解锁
我们直接让new=当前状态-mutexLocked
如果new=0, 则意味着只有当前goroutine在持有锁, 且无任何goroutine在等待锁, 那么直接CAS修改m.state=new然后返回即可.
慢速解锁
否则调用unlockSlow()函数来解锁, unlockSlow()函数也会根据当前Mutex的不同状态做出不同的行为
不过首先, unlockSlow()会判断当前Mutex是否处于上锁态, 如果我们对未上锁的Mutex调用Unlock()函数, 会爆出sync: unlock of unlocked mutex
的panic.
正常状态
通过state的前28位判断当前等待锁的goroutine是否为0, 如果是, 那么直接解锁返回.
否则通过CAS的方式修改当前Mutex状态为new, 如果修改成功, 那么将释放一个信号量来随机唤醒一个阻塞在sema中的goroutine
//false意味着随机唤醒 runtime_Semrelease(&m.sema, false, 1)
饥饿状态
如果当前Mutex处于饥饿状态, 那么说明一定存在阻塞的goroutine, 将释放一个信号量来唤醒sema中第一个阻塞的goroutine
//true为顺序唤醒 runtime_Semrelease(&m.sema, true, 1)
TryLock是尝试上锁, 如果上锁成功, 返回true, 否则返回false, 他的实现十分简单.
判断当前状态Mutex的状态是否是饥饿态或者已上锁, 如果是, 则直接返回false
通过CAS的方式尝试为Mutex上锁, 上锁成功则返回true,否则返回false
“Golang sync包之sync.Mutex怎么使用”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。