Go 1.19.3 sync.Mutex原理简析
admin
2024-05-14 05:02:32
0

互斥锁

互斥,数学名词,事件A与事件B在任何一次试验中都不会同时发生,则称事件A与事件B互斥。(来自百度百科)
互斥锁的出现时为了保护共享资源,同一时刻只有锁的持有者才能操作该资源。

Go Mutex

以下代码均来自src/sync/mutex.go
该包中定义了两个外部函数,用于panic操作

// Provided by runtime via linkname.
func throw(string)
func fatal(string)

Locker 接口 定义了锁的行为,锁定与解锁

// A Locker represents an object that can be locked and unlocked.
type Locker interface {Lock()   //锁定Unlock() //解锁
}

Mutext 结构体实现了Locker接口,作为互斥锁使用

// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex. 
//
// A Mutex must not be copied after first use.
//
// In the terminology of the Go memory model,
// the n'th call to Unlock “synchronizes before” the m'th call to Lock
// for any n < m.
// A successful call to TryLock is equivalent to a call to Lock.
// A failed call to TryLock does not establish any “synchronizes before”
// relation at all.
type Mutex struct {state int32sema  uint32
}

互斥锁的零值代表该锁是解锁状态。在Go内存模型中,第n次调用Unlock"在第m次调用Lock之前同步",对于任何n都是小于m的。成功调用TryLock相当于调用Lock。对TryLock的失败调用不建立任何“先行同步”关系。

Mutex中的两个字段分别是:
sema:信号量,在锁饥饿状态下使用
state: 表示锁的状态,其为一个int32类型位图,该位图有32个bit。
位图的状态如下:
mutexLocked:低1位表示锁定状态 1代表锁定,0代表未锁定
mutexWoken:低2位表示锁唤醒状态,协程在正常状态下被唤醒
mutexStarving:低3位表示锁饥饿状态
mutexWaiterShift:低4位之后表示等待被唤醒的协程数量

const (mutexLocked = 1 << iota // mutex is lockedmutexWokenmutexStarvingmutexWaiterShift = iota// Mutex fairness.// 锁公平性// Mutex can be in 2 modes of operations: normal and starvation.// 锁有两种模式,正常和饥饿// In normal mode waiters are queued in FIFO order, // 在正常模式下,等待者按FIFO顺序排队//but a woken up waiter does not own the mutex and competes with new arriving goroutines over the ownership. // 但被唤醒的等待者并不拥有互斥锁,而是与新到的goroutine争夺所有权。// New arriving goroutines have an advantage -- they are// already running on CPU and there can be lots of them, so a woken up// waiter has good chances of losing. // 新来的goroutine有一个优势——他们是已经在CPU上运行,并且可能有很多,所以醒来了等待者很有可能无法抢占。//In such case it is queued at front of the wait queue.// 在这种情况下,它在前面排队等待队列的。//If a waiter fails to acquire the mutex for more than 1ms, it switches mutex to the starvation mode.//如果等待者在超过1ms的时间内未能获取互斥锁,它将互斥锁切换到饥饿模式// In starvation mode ownership of the mutex is directly handed off from the unlocking goroutine to the waiter at the front of the queue.// 在饥饿模式下,互斥锁的所有权直接从解锁的goroutine移交给队列前面的等待者// New arriving goroutines don't try to acquire the mutex even if it appears to be unlocked, and don't try to spin. Instead they queue themselves at the tail of the wait queue.// 新到达的goroutine不会尝试获取互斥对象,即使它出现了//不要试图旋转。相反,他们在等待队列的尾部// If a waiter receives ownership of the mutex and sees that either// 如果等待者收到互斥锁的所有权,并看到// (1) it is the last waiter in the queue, // (1) 这是排队的最后一个等待者// or (2) it waited for less than 1 ms, it switches mutex back to normal operation mode.//或(2)等待少于1ms,它将互斥切换回正常操作模式// Normal mode has considerably better performance as a goroutine can acquire a mutex several times in a row even if there are blocked waiters.// 普通模式的性能要比常规模式好得多即使有被阻塞的等待者,互斥锁也会连续出现几次// Starvation mode is important to prevent pathological cases of tail latency.// 饥饿模式对于预防尾部延迟的情况非常重要starvationThresholdNs = 1e6
)

starvationThresholdNs:表示等待时间的判定值,1 * 10 ^ 6 纳秒

(m *Mutex) Lock() 上锁

Lock时会首先尝试使用CAS操作进行锁的状态更新,若成功则直接返回,代表上锁成功。若失败则执行慢路径,慢路径中若自旋长时间获取不到锁,则使用信号量进行同步操作。

// Lock locks m.  Lock 锁定m
// If the lock is already in use, the calling goroutine
// blocks until the mutex is available.
// 如果锁已在使用,则调用的goroutine将阻塞,直到可以获得互斥锁。
func (m *Mutex) Lock() {// Fast path: grab unlocked mutex.  快速路径,获取未锁定的互斥锁if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { //若锁的状态为0,代表未上锁,则cas操作将锁的状态改为mutexLocked,即1。代表已经上锁if race.Enabled { // race相关的race.Acquire(unsafe.Pointer(m))}return // 抢占成功,则直接返回}// 抢占不成功进入自旋状态尝试抢占// Slow path (outlined so that the fast path can be inlined)m.lockSlow() // 慢路径,做自旋操作
}

(m *Mutex) lockSlow() 慢路径尝试上锁

func (m *Mutex) lockSlow() {var waitStartTime int64 //等待开始时间以纳秒计starving := false  // 是否饥饿awoke := false     // 是否是醒着的 iter := 0          // 迭代次数old := m.state     // 旧的状态for { //开始自旋// Don't spin in starvation mode, ownership is handed off to waiters// so we won't be able to acquire the mutex anyway.// 不可以在饥饿状态下自旋,所有权移交给等待者,我们无论如何都无法获取互斥锁if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {  // 若 旧状态中的 只有锁定的状态位,饥饿的状态位为空,并且可以尝试自旋iter次则执行下边操作// Active spinning makes sense. 主动旋转有意义// Try to set mutexWoken flag to inform Unlock// to not wake other blocked goroutines. 尝试设置mutexWoken标志以通知Unlock不唤醒其他被阻塞的goroutine// 如果非唤醒状态,并且mutexWoken状态位为空,并且等待者数量非0,并且cas操作旧状态与当前状态一致,且可以更新状态位mutexWoken锁唤醒状态。则设置awoke 为true,表示是醒着的if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {awoke = true}runtime_doSpin() //做自旋操作,等待抢占iter++ //尝试次数+1old = m.state // 更新旧的状态continue // 进入下一轮自旋}// 不满足自旋条件new := old // 转移状态// Don't try to acquire starving mutex, new arriving goroutines must queue.不要试图获取饥饿的互斥体,新到达的goroutine必须排队。if old&mutexStarving == 0 { // 若旧状态中无饥饿状态new |= mutexLocked // 设置锁定状态}if old&(mutexLocked|mutexStarving) != 0 { // 锁定状态或饥饿状态或叠加,等待者个数增加new += 1 << mutexWaiterShift}// The current goroutine switches mutex to starvation mode.// But if the mutex is currently unlocked, don't do the switch.// Unlock expects that starving mutex has waiters, which will not// be true in this case.//当前的goroutine将互斥切换到饥饿模式。//但如果互斥锁当前未锁定,则不要进行切换。//Unlock期望饥饿的互斥体有等待者,而等待者不会在这种情况下是正确的if starving && old&mutexLocked != 0 { //饥饿 并且 已锁定new |= mutexStarving //置饥饿状态}if awoke { //若醒着状态// The goroutine has been woken from sleep, goroutine 已被唤醒// so we need to reset the flag in either case. 重置标记为if new&mutexWoken == 0 { // 有其他goroutine修改了状态,panicthrow("sync: inconsistent mutex state")}new &^= mutexWoken //清空状态位}if atomic.CompareAndSwapInt32(&m.state, old, new) { //尝试修改状态if old&(mutexLocked|mutexStarving) == 0 { // 旧状态非锁定或饥饿状态break // locked the mutex with CAS  用cas操作上锁成功,返回}// If we were already waiting before, queue at the front of the queue. 如果我们之前已经在等待,请在队列的前面排队。queueLifo := waitStartTime != 0 //判定第一次循环时间状态if waitStartTime == 0 { //赋值时间waitStartTime = runtime_nanotime()}runtime_SemacquireMutex(&m.sema, queueLifo, 1) //用信号量抢占starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs //饥饿 或 等待时间过长old = m.state //状态转移if old&mutexStarving != 0 { //锁饥饿状态// If this goroutine was woken and mutex is in starvation mode, 如果这个goroutine被唤醒并且互斥锁处于饥饿模式// ownership was handed off to us but mutex is in somewhat// inconsistent state: mutexLocked is not set and we are still// accounted as waiter. Fix that.// 所有权已移交给我们,但互斥锁在某种程度上不一致的状态:互斥锁未设置,我们仍然作为等待者。解决这个问题。if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 { //状态不一致,panicthrow("sync: inconsistent mutex state")}delta := int32(mutexLocked - 1<>mutexWaiterShift == 1 { //非饥饿,只有一个等待者// Exit starvation mode.// Critical to do it here and consider wait time.// Starvation mode is so inefficient, that two goroutines// can go lock-step infinitely once they switch mutex// to starvation mode.//退出饥饿模式。//在这里进行此操作并考虑等待时间至关重要。//饥饿模式是如此低效,以至于两次//一旦切换互斥锁,就可以无限地锁步//到饥饿模式。delta -= mutexStarving  //清除饥饿状态}atomic.AddInt32(&m.state, delta) //清除状态break}awoke = true  //唤醒iter = 0 // 重置迭代计数器} else {old = m.state //状态迁移}}if race.Enabled { // race相关race.Acquire(unsafe.Pointer(m))}
}

(m *Mutex) TryLock() 尝试上锁,可以的话就加锁,不可以就返回

// TryLock tries to lock m and reports whether it succeeded.TryLock尝试锁定m并报告是否成功。
//
// Note that while correct uses of TryLock do exist, they are rare,
// and use of TryLock is often a sign of a deeper problem
// in a particular use of mutexes.
// 请注意,虽然TryLock的正确用法确实存在,
// 而TryLock的使用往往是一个更深层次问题的标志
// 在互斥体的特定使用中。
func (m *Mutex) TryLock() bool {old := m.state  //状态转移if old&(mutexLocked|mutexStarving) != 0 { // 旧状态为锁定或锁饥饿,或叠加态,则直接返回return false}// There may be a goroutine waiting for the mutex, but we are// running now and can try to grab the mutex before that// goroutine wakes up.//可能有一个goroutine在等待互斥锁,但我们是//现在正在运行,并且可以尝试在此之前获取互斥锁goroutine醒来了。if !atomic.CompareAndSwapInt32(&m.state, old, old|mutexLocked) { //尝试解锁,更新状态,若不成功,则返回false,否则后续返回truereturn false}if race.Enabled { //race相关race.Acquire(unsafe.Pointer(m))}return true
}

(m *Mutex) Unlock() 解锁

解锁操作首先尝试用原子操作进行锁的状态更新,若成功,则直接返回代表解锁成功,若失败则进入慢路径,使用自旋和信号量释放的方式尝试解锁。

// Unlock unlocks m. Unlock解锁m
// It is a run-time error if m is not locked on entry to Unlock.
// 如果m在进入解锁时未被锁定,这是一个运行时错误。// A locked Mutex is not associated with a particular goroutine.
// 锁定的Mutex与特定的goroutine没有关联。
// It is allowed for one goroutine to lock a Mutex and then
// arrange for another goroutine to unlock it.
// 允许一个goroutine锁定Mutex,然后安排另一个goroutine来解锁它。
func (m *Mutex) Unlock() {if race.Enabled {_ = m.staterace.Release(unsafe.Pointer(m))}// Fast path: drop lock bit.new := atomic.AddInt32(&m.state, -mutexLocked) // 快速路径用cas操作尝试去掉mutexLocked状态位if new != 0 { // 解锁失败,或存在其他状态,进入慢路径// Outlined slow path to allow inlining the fast path.// To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock. 为了在跟踪过程中隐藏解锁,我们在跟踪GoUnblock时跳过一个额外的帧。m.unlockSlow(new)}
}

(m *Mutex) unlockSlow(new int32) 解锁的慢路径

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 there are no waiters or a goroutine has already// been woken or grabbed the lock, no need to wake anyone.//如果没有等待者,或者已经有一个goroutine被唤醒或持有住锁,无需唤醒任何人。// In starvation mode ownership is directly handed off from unlocking// goroutine to the next waiter. // 在饥饿模式下,所有权直接从解锁转移到下一个等待者。// We are not part of this chain, since we did not observe mutexStarving when we unlocked the mutex above.//我们不是这个链的一部分,因为我们在解锁上面的互斥锁时没有观察到互斥锁饥饿。// So get off the way. 所以别挡路if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 { //没有等待者,并且锁定或唤醒或饥饿状态或叠加状态return //直接返回}// Grab the right to wake someone. 唤醒某个goroutinenew = (old - 1< //可以更新状态runtime_Semrelease(&m.sema, false, 1) //释放信号量return // 返回}old = m.state // 状态迁移}} else {// Starving mode: handoff mutex ownership to the next waiter, and yield// our time slice so that the next waiter can start to run immediately.//饥饿模式:将互斥体所有权移交给下一个等待者,并让位我们的时间片,以便下一个等待者可以立即开始运行。// Note: mutexLocked is not set, the waiter will set it after wakeup.// 注意:互斥锁未设置,等待者将在唤醒后设置。// But mutex is still considered locked if mutexStarving is set,// so new coming goroutines won't acquire it.// 但如果设置了mutexStaring,则互斥锁仍被认为是锁定的,// 所以新来的goroutine不会获得它runtime_Semrelease(&m.sema, true, 1) // 释放信号量}
}

总结

sync.Mutex是使用CAS原子操作,自旋,信号量同步的方式实现的,其结构体内部使用位图存储锁的状态。这是一把重量级的锁。在同步过程中可以保证锁内代码状态的唯一性。Mutex一经创建不能被复制。在读多写少的场景中,可以使用sync.RWMutex做同步操作。

相关内容

热门资讯

4月份银行理财规模环比增加2.... 钛媒体App 5月16日消息,银行理财市场在4月份迎来规模与收益的双增长。据华源证券廖志明团队发布的...
【光明日报】黑龙江:免签红利释... 5月10日早上7时,一辆国际大巴缓缓停靠在黑龙江绥芬河公路口岸入境大厅前。游客们提着大包小裹,依次走...
又一跨国高端化工合作项目落子乐... 5月15日,福华化学携手瑞士特种化学品企业科莱恩打造的创新型高端磷系无卤阻燃剂项目(以下简称“福华科...
鸿蒙智行:已拥有1951家销售... IT之家 5 月 15 日消息,鸿蒙智行智界 V9 发布会正在进行,官方透露目前已拥有 1951 家...
黄金、白银,直线大跌! 5月15日晚间,贵金属价格突然大跌! 截至记者发稿时,现货黄金跌超2%,暂报4553美元/盎司附近。...
央视《焦点访谈》聚焦!万兴科技... 深圳商报·读创客户端首席记者 谢惠茜 5月14日,中央电视台《焦点访谈》推出专题节目《扩能提质强服务...
东方嘉富人寿董事长履职半年被换... 文|达摩财经 东方嘉富人寿再度进行人事调整。 5月13日,东方嘉富人寿发布公告称,自2026年4月...
重返西决!文班19+6卡斯尔3... 【搜狐体育战报】北京时间5月16日NBA季后赛,客场作战的马刺以139-109击败森林狼,总比分4-...
原创 美... 十万亿美债为什么还没有崩盘?或许答案在于,中国的存在让局势与众不同。现在的美债就像一张看似脆弱的网,...
原创 茅... 最近打开股票软件看白酒板块,是不是心里拔凉拔凉的? 茅台又回到1300元区间了,五粮液跌破90元,洋...
茅台宣布涨价 5月15日深夜,“i茅台”APP发布公告称,按照随行就市、供需适配、量价平衡、相对平稳的原则,贵州茅...
最高涨200元!茅台官宣4款产... 贵州茅台(600519.SH)凌晨宣布涨价几款产品。 茅台数字营销平台“i茅台”今日(5月16日)发...
面向地方国资产融转型全链条,X... 5月15日,XOD创新投融资模式3.0产品发布会在广州举办。该产品主要面向地方国资产融协同创新转型提...
2026Q1:10家上市商超9... 截至4月30日,所有A股上市公司2026年Q1财报全部出炉,传统商超也晒出自己的成绩单。10家披露的...
入主盟科药业失利后,拟借款2.... 来源:时代周报-时代在线 继去年试图通过定增入主盟科药业(688373.SH)失败后,海鲸药业再度出...
同比激增86%、规模突破760... 图片来源:界面图库 界面新闻记者 | 孙艺真 今年以来,证券行业融资补血热潮持续升温。前5个月...
促进青年消费,扶持青年创业,上... 5月14日,上海市政协团青界别、经济界跨界别活动在市政协全过程人民民主实践点举行。 今年初,团市委立...
苹果股价昨日创收盘新高,站上3... IT之家 5 月 16 日消息,苹果公司股价昨日(5 月 15 日)收于 300.23 美元,首次站...
杭州首批配售型保障房正式入市 杭州首批配售型保障房正式入市 价格约为周边商品房5折,18日开始报名 不能入市交易,可由政府指定机构...
“后巴菲特时代”,伯克希尔调仓... 当地时间5月15日,伯克希尔披露了2026年一季度美股持仓报告。这是伯克希尔在巴菲特卸任CEO并由阿...