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做同步操作。

相关内容

热门资讯

君乐宝上市布局细分赛道领跑增长... 2026年1月19日,中国领先的综合乳制品企业君乐宝乳业集团股份有限公司(简称“君乐宝”)正式向香港...
酒业渠道商再闯港股,名品世家董... 蓝鲸新闻2月10日讯(记者 朱欣悦)2月10日,港股GEM上市公司环球印馆控股有限公司(08448....
上市银行,迎密集调研! 2026年以来,上市银行迎来机构密集调研,其中沿海经济发达区域的中小银行是机构重点调研对象。 截至2...
豆包压哨参战,决战春节流量窗口... 本报(chinatimes.net.cn)记者卢晓 北京报道 春节AI红包大战继续升温。 2月10日...
小米汽车准备进入美国市场?雷军... 2月10日,针对小米YU7被拍到行驶在美国加州的高速公路上,挂着当地的测试车牌或进入美国市场的消息,...
春节后部分黄金产品将调价?周大... 2月10日,有消息称,黄金珠宝品牌周大福在春节后将对黄金产品调价,此次调价或于3月中旬启动,目前部分...
标普500指数高开9.67点,... 标普500指数高开9.67点,涨幅0.14%,报6974.49点; 道琼斯工业平均指数高开57.6...
中国银行调整春节期间代理个人上... 中国银行2月10日发布关于2026年春节期间代理个人上金所业务相关调整的公告称,2026年春节假期临...
雷军:小米汽车工厂有六七百台机... 小米集团创始人、董事长兼CEO雷军2月10日晚在小年夜直播中表示,小米汽车工厂第一个特点是高度智能化...
锅圈创始人杨明超,正式担任河南... 锅圈创始人杨明超,正式担任河南省宋河酒业股份有限公司董事长! 国家企业信用信息公示系统显示,近日,宋...
Uber斥资3.35亿美元收购... Uber公司周一宣布,已同意收购土耳其Getir公司的配送业务。Getir曾是土耳其初创企业生态系统...
潍坊临朐:民企“出海”如何破圈... 岁末年初,潍坊临朐处处涌动着发展的热潮,一批聚焦细分领域的科技型民企,凭借持续研发投入与技术迭代,将...
雷军揭秘小米电机突破两万转背后... IT之家 2 月 10 日消息,小米创办人,董事长兼 CEO 雷军今晚举行小年夜直播,谈及新一代小米...
聚智共研启新程 法护网界谱华章 本报记者 广辉 为深入贯彻落实新时代司法改革要求,破解互联网司法实践难题,推动互联网法院(法庭)高质...
无缘卫冕!短道速滑混合团体接力... 北京时间2月10日,米兰冬奥会进入第五日。在米兰滑冰馆进行了短道速滑混合团体接力决赛中,由张楚桐、公...
被神化的灵芝孢子油保健品:所谓... 近几年,灵芝孢子油在保健品市场迅速走红,销量持续攀升。随着市场规模扩大,企业之间的竞争也愈发激烈。为...
提名美联储主席,沃什会否改写全... 凯文·沃什曾任职摩根士丹利并购部,2006年-2011年担任美联储理事。2026年1月30日,他获特...