本篇内容介绍了“Golang中sync.Mutex源码分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
type Mutex struct {
state int32
sema uint32
}
state 记录锁的状态,转换为二进制前29位表示等待锁的goroutine数量,后三位从左到右分别表示当前g 是否已获得锁、是否被唤醒、是否正饥饿
sema 充当临界资源,其地址作为这个锁在全局的唯一标识,所有等待这个锁的goroutine都会在阻塞前把自己的sudog放到这个锁的等待队列上,然后等待被唤醒,sema的值就是可以被唤醒的goroutine的数目,只有0和1。
const (
mutexLocked = 1 << iota // mutex is locked //值1,转二进制后三位为001,表示锁已被抢
mutexWoken //值2,转二进制后三位为010,告诉即将释放锁的g现在已有g被唤醒
mutexStarving //值4,转二进制后三位为100,表示当前处在饥饿状态
mutexWaiterShift = iota //值3,表示mutex.state右移3位为等待锁的goroutine数量
starvationThresholdNs = 1e6 //表示mutext切换到饥饿状态所需等待时间的阈值,1ms。
)
type Locker interface {
Lock()
Unlock()
}
下面重点看这两个方法。
func (m *Mutex) Lock() {
// 第一种情况:快上锁,即此刻无人来抢锁
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled { //竞争检测相关,不用看
race.Acquire(unsafe.Pointer(m))
}
return
}
// 第二种情况:慢上锁,即此刻有竞争对手
m.lockSlow()
}
CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool){},go的CAS操作,底层通过调用cpu指令集提供的CAS指令实现,位置在src/runtime/internal/atomic/atomic_amd64.s/·Cas(SB)。
参数addr:变量地址
参数old:旧值
参数new:新值
原理:如果addr和old相等,则将new赋值给addr,并且返回true,否则返回false
// 注释里的第一人称“我”只当前g
func (m *Mutex) lockSlow() {
var waitStartTime int64 // 等待开始的时间
starving := false // 我是否饥饿
awoke := false // 我是否被唤醒
iter := 0 // 我的自旋次数
old := m.state // 这个锁此时此刻所有的信息
for {
// 如果:锁已经被抢了 或着 正处在饥饿状态 或者 允许我自旋 那么进行自旋
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// 如果:我没有处在唤醒态 并且 当前无g处在唤醒态 并且
// 有等待锁的g 并且CAS尝试将我置为唤醒态成功 则进行自旋
// 之所以将我置为唤醒态是为了明示那些执行完毕正在退出的g不用再去唤醒其它g了,因为只允许存在一个唤醒的g。
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin() // 我自旋一次
iter++ // 我自旋次数加1
old = m.state
continue
}
new := old // new只是个中间态,后面的cas操作将会判断是否将这个中间态落实
// 如果不是处在饥饿模式就立即抢锁
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 如果锁被抢了 或者 处在饥饿模式,那就去排队
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift // 等待锁的goroutine数量加1
}
// 如果我现在饥渴难耐 而且 锁也被抢走了,那就立即将锁置为饥饿模式
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
if awoke {
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
// 释放我的唤醒态
// 因为后面我要么抢到锁要么被阻塞,都不是处在和唤醒态
new &^= mutexWoken
}
//此处CAS操作尝试将new这个中间态落实
if atomic.CompareAndSwapInt32(&m.state, old, new) {
if old&(mutexLocked|mutexStarving) == 0 {
break // 抢锁成功!
}
// queueLifo我之前有没有排过队
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
//原语:如果我之前排过队,这次就把我放到等待队列队首,否则把我放到队尾,并将我挂起
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
// 刚被唤醒的我先判断自己是不是饥饿了,如果我等待锁的时间小于starvationThresholdNs(1ms),那就不饿
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
if old&mutexStarving != 0 {
// 我一觉醒来发觉锁正处在饥饿状态,苍天有眼这个锁属于我了,因为饥饿状态绝对没有人跟我抢锁
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
// delta是一个中间状态,atomic.AddInt32方法将给锁落实这个状态
delta := int32(mutexLocked - 1<<mutexWaiterShift)
if !starving || old>>mutexWaiterShift == 1 {
// 如果现在我不饥饿或者等待锁的就我一个,那么就将锁切换到正常状态。
// 饥饿模式效率很低,而且一旦有两个g把mutex切换为饥饿模式,那就会死锁。
delta -= mutexStarving
}
// 原语:给锁落实delta的状态。
atomic.AddInt32(&m.state, delta)
// 我拿到锁啦
break
}
// 把我的状态置为唤醒,我将继续去抢锁
awoke = true
// 把我的自旋次数置0,我又可以自旋抢锁啦
iter = 0
} else {
// 继续去抢锁
old = m.state
}
}
// 竞争检测的代码,不管
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}
runtime_canSpin(iter) 判断当前g可否自旋,已经自旋过iter次
func sync_runtime_canSpin(i int) bool {
// 可自旋的条件:
// 1.多核cpu
// 2.GOMAXPROCS > 1 且 至少有一个其他的p在运行 且 该p的本地runq为空
// 3.iter小于最大自旋次数active_spin = 4
if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {
return false
}
if p := getg().m.p.ptr(); !runqempty(p) {
return false
}
return true
}
runtime_doSpin()通过调用procyield(n int32)方法来实现空耗CPU,n乃空耗CPU的次数。
//go:linkname sync_runtime_doSpin sync.runtime_doSpin
//go:nosplit
func sync_runtime_doSpin() {
procyield(active_spin_cnt)
}
procyield(active_spin_cnt) 的底层通过执行PAUSE指令来空耗30个CPU时钟周期。
TEXT runtime·procyield(SB),NOSPLIT,$0-0
MOVL cycles+0(FP), AX
again:
PAUSE
SUBL $1, AX
JNZ again
RET
runtime_SemacquireMutex(&m.sema, queueLifo, 1) 将当前g放到mutex的等待队列中去
//go:linkname sync_runtime_SemacquireMutex sync.runtime_SemacquireMutex
func sync_runtime_SemacquireMutex(addr *uint32, lifo bool, skipframes int) {
semacquire1(addr, lifo, semaBlockProfile|semaMutexProfile, skipframes)
}
semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int) 若lifo为true,则把g放到等待队列队首,若lifo为false,则把g放到队尾
atomic.AddInt32(int32_t *val, int32_t delta) 原语:给t加上t_delta
uint32_t
AddUint32 (uint32_t *val, uint32_t delta)
{
return __atomic_add_fetch (val, delta, __ATOMIC_SEQ_CST);
}
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// 如果没有g在等待锁则立即释放锁
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
// 如果还有g在等待锁,则在锁释放后需要做一点收尾工作。
m.unlockSlow(new)
}
}
func (m *Mutex) unlockSlow(new int32) {
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
// 如果锁处在正常模式下
if new&mutexStarving == 0 {
old := new
for {
// 如果锁正处在正常模式下,同时 没有等待锁的g 或者 已经有g被唤醒了 或者 锁已经被抢了,就什么也不用做直接返回
// 如果锁正处在饥饿模式下,也是什么也不用做直接返回
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// 给锁的唤醒标志位置1,表示已经有g被唤醒了,Mutex.state后三位010
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 唤醒锁的等待队列头部的一个g
// 并把g放到p的funq尾部
runtime_Semrelease(&m.sema, false, 1)
return
}
old = m.state
}
} else {
// 锁处在饥饿模式下,直接唤醒锁的等待队列头部的一个g
//因为在饥饿模式下没人跟刚被唤醒的g抢锁,所以不用设置锁的唤醒标志位
runtime_Semrelease(&m.sema, true, 1)
}
}
runtime_Semrelease(&m.sema, false, 1) 用来释放mutex等待队列上的一个g
//go:linkname sync_runtime_Semrelease sync.runtime_Semrelease
func sync_runtime_Semrelease(addr *uint32, handoff bool, skipframes int) {
semrelease1(addr, handoff, skipframes)
}
semrelease1(addr, handoff, skipframes) 参数handoff若为true,则让被唤醒的g立刻继承当前g的时间片继续执行。若handoff为false,则把刚被唤醒的g放到当前p的runq中。
“Golang中sync.Mutex源码分析”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。
原文链接:https://www.cnblogs.com/usmiles/p/15605827.html