设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间00 现在时间00 缓存数据 对自己狠一点,逼自己努力,再过几年你将会感谢今天发狠的自己、恨透今天懒惰自卑的自己。晚安!

对自己狠一点,逼自己努力,再过几年你将会感谢今天发狠的自己、恨透今天懒惰自卑的自己。晚安!

查看: 1402|回复: 4

Golang Mutex实现互斥的具体方法

[复制链接]

  离线 

TA的专栏

  • 打卡等级:无名新人
  • 打卡总天数:1
  • 打卡月天数:0
  • 打卡总奖励:18
  • 最近打卡:2025-08-22 18:42:50
等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
31
主题
29
精华
0
金钱
118
积分
62
注册时间
2023-9-30
最后登录
2025-8-31

发表于 2024-3-8 21:06:34 | 显示全部楼层 |阅读模式
目录


  • 获取锁

    • 未锁——直接获取
    • 在不饥饿且旋的不多的情况下,尝试自旋
    • 自旋究竟在做什么呢?
    • 计算期望状态
    • 尝试达成获取锁期望
    • 考虑几种场景

  • 释放锁

    • 只有已锁——直接释放
    • 慢释放

Mutex是Golang常见的并发原语,不仅在开发过程中经常使用到,如channel这种具有golang特色的并发结构也依托于Mutex从而实现
  1. type Mutex struct {
  2.   // 互斥锁的状态,比如是否被锁定
  3.   state int32
  4.   // 表示信号量。堵塞的协程会等待该信号量,解锁的协程会释放该信号量
  5.   sema int32
  6. }
复制代码
  1. const (
  2.   // 当前是否已经上锁
  3.   mutexLocked = 1 << iota // 1
  4.   // 当前是否有唤醒的goroutine
  5.   mutexWoken // 2
  6.   // 当前是否为饥饿状态
  7.   mutexStarving // 4
  8.   // state >> mutexWaiterShift 得到等待者数量
  9.   mutexWaiterShift = iota // 3

  10.   starvationThresholdNs = 1e6 // 判断是否要进入饥饿状态的阈值
  11. )
复制代码
Mutex有正常饥饿模式。

  • 正常模式:等待者会入队,但一个唤醒的等待者不能持有锁,以及与新到来的goroutine进行竞争。新来的goroutine有一个优势——他们已经运行在CPU上。
    超过1ms没有获取到锁,就会进入饥饿模式
  • 饥饿模式:锁的所有权直接移交给队列头goroutine,新来的goroutine不会尝试获取互斥锁,即使互斥锁看起来已经解锁,也不会尝试旋转。相反,他们自己排在等待队列的末尾。
若等待者是最后一个,或者等待小于1ms就会切换回正常模式

获取锁


未锁——直接获取
  1. func (m *Mutex) Lock() {
  2.     // 快路径。直接获取未锁的mutex
  3.   // 初始状态为0,所以只要状态存在其他任何状态位都是无法直接获取的
  4.     if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
  5.         if race.Enabled {
  6.             race.Acquire(unsafe.Pointer(m))
  7.         }
  8.         return
  9.     }
  10.     // Slow path (outlined so that the fast path can be inlined)
  11.     m.lockSlow()
  12. }
复制代码
在不饥饿且旋的不多的情况下,尝试自旋
  1.         // 只要原状态已锁且不处于饥饿状态,并满足自旋条件
  2.         if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
  3.             // 在当前goroutine没有唤醒,且没有其他goroutine在尝试唤醒,且存在等待的情况下,cas标记存在goroutine正在尝试唤醒。若标记成功就设置当前goroutine已经唤醒了
  4.             if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
  5.                 atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
  6.                 awoke = true
  7.             }
  8.       // 自旋
  9.             runtime_doSpin()
  10.       // 自旋次数加一
  11.             iter++
  12.       // 更新原状态
  13.             old = m.state
  14.             continue
  15.         }
复制代码
具体的自旋条件如下

  • 自旋次数小于4
  • 多核CPU
  • p数量大于1
  • 至少存在一个p的队列为空
  1. const (
  2.     locked uintptr = 1

  3.     active_spin     = 4
  4.     active_spin_cnt = 30
  5.     passive_spin    = 1
  6. )

  7. func sync_runtime_canSpin(i int) bool {
  8.     // sync.Mutex is cooperative, so we are conservative with spinning.
  9.     // Spin only few times and only if running on a multicore machine and
  10.     // GOMAXPROCS>1 and there is at least one other running P and local runq is empty.
  11.     // As opposed to runtime mutex we don't do passive spinning here,
  12.     // because there can be work on global runq or on other Ps.
  13.     if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {
  14.         return false
  15.     }
  16.     if p := getg().m.p.ptr(); !runqempty(p) {
  17.         return false
  18.     }
  19.     return true
  20. }
复制代码
自旋究竟在做什么呢?

自旋是由方法runtime_doSpin()执行的,实际调用了procyield()
  1. # 定义了一个runtime.procyield的文本段,通过NOSPLIT不使用栈分裂,$0-0 表示该函数不需要任何输入参数和返回值
  2. TEXT runtime·procyield(SB),NOSPLIT,$0-0
  3.     # 从栈帧中读取cycles参赛值,并储存在寄存器R0中
  4.     MOVWU    cycles+0(FP), R0
  5. # 组成无限循环。在每次循环中,通过YIELD告诉CPU将当前线程置于休眠状态
  6. # YIELD: x86上,实现为PAUSE指令,会暂停处理器执行,切换CPU到低功耗模式并等待更多数据到达。通常用于忙等待机制,避免无谓CPU占用
  7. # ARM上,实现为WFE(Wait For Event),用于等待中断或者其他事件发生。在某些情况下可能会导致CPU陷入死循环,因此需要特殊处理逻辑解决
  8. again:
  9.     YIELD
  10.     # 将R0值减1
  11.     SUBW    $1, R0
  12.     # CBNZ(Compare and Branch on Non-Zero)检查剩余的时钟周期数是否为0。不为0就跳转到标签again并再次调用YIELD,否则就退出函数
  13.     CBNZ    R0, again
  14.     RET
复制代码
以上汇编代码分析过程感谢chatgpt的大力支持
从代码中可以看到自旋次数是30次
  1. const active_spin_cnt = 30

  2. func sync_runtime_doSpin() {
  3.     procyield(active_spin_cnt)
  4. }
复制代码
计算期望状态

1.原状态不处于饥饿状态,新状态设置已锁状态位
原状态处于已锁状态或饥饿模式,新状态设置等待数量递增
  1. 当前goroutine是最新获取锁的goroutine,在正常模式下期望就是要获取锁,那么就应该设置新状态已锁状态位
复制代码
如果锁已经被抢占了,或者处于饥饿模式,那么就应该去排队
2.若之前尝试获取时已经超过饥饿阈值时间,且原状态已锁,那么新状态设置饥饿状态位
3.若goroutine处于唤醒,则新状态清除正在唤醒状态位
  1. 期望是已经获取到锁了,那么自然要清除正在获取的状态位
复制代码
  1.         new := old
  2.         // Don't try to acquire starving mutex, new arriving goroutines must queue.
  3.     // 若原状态不处于饥饿状态,就给新状态设置已加锁
  4.         if old&mutexStarving == 0 {
  5.             new |= mutexLocked
  6.         }
  7.     // 只要原状态处于已锁或者饥饿模式,就将新状态等待数量递增
  8.         if old&(mutexLocked|mutexStarving) != 0 {
  9.             new += 1 << mutexWaiterShift
  10.         }
  11.     // 若已经等待超过饥饿阈值时间且原状态已锁,就设置新状态为饥饿
  12.     // 这也意味着如果已经不处于已锁状态,就可以切换回正常模式了
  13.         if starving && old&mutexLocked != 0 {
  14.             new |= mutexStarving
  15.         }
  16.     // 如果已经唤醒了(也就是没有其他正在抢占的goroutine),则在新状态中清除正在唤醒状态位
  17.         if awoke {
  18.             // The goroutine has been woken from sleep,
  19.             // so we need to reset the flag in either case.
  20.             if new&mutexWoken == 0 {
  21.                 throw("sync: inconsistent mutex state")
  22.             }
  23.             new &^= mutexWoken
  24.         }
复制代码
尝试达成获取锁期望

cas尝试从原状态更新为新的期望状态
如果失败,则更新最新状态,继续尝试获取锁
  1. 说明这期间锁已经被抢占了
复制代码
若原来既没有被锁住,也没有处于饥饿模式,那么就获取到锁,直接返回
排队。若之前已经在等待了就排到队列头
获取信号量。此处会堵塞等待
被唤醒,认定已经持有锁。并做以下饥饿相关处理

  • 计算等待时长,若超出饥饿阈值时间,就标记当前goroutine处于饥饿
  • 若锁处于饥饿模式,递减等待数量,并且在只有一个等待的时候,切换锁回正常模式
  1. if atomic.CompareAndSwapInt32(&m.state, old, new) {
  2.       // 如果原状态既没有处于已锁状态,也没有处于饥饿模式
  3.       // 那么就表示已经获取到锁,直接退出
  4.             if old&(mutexLocked|mutexStarving) == 0 {
  5.                 break // locked the mutex with CAS
  6.             }
  7.       // 若已经在等待了,就排到队列头
  8.             queueLifo := waitStartTime != 0
  9.             if waitStartTime == 0 {
  10.                 waitStartTime = runtime_nanotime()
  11.             }
  12.       // 尝试获取信号量。此处获取一个信号量以实现互斥
  13.       // 此处会进行堵塞
  14.             runtime_SemacquireMutex(&m.sema, queueLifo, 1)
  15.       // 被信号量唤醒之后,发现若等待时间超过饥饿阈值,就切换到饥饿模式
  16.             starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
  17.             old = m.state
  18.       // 处于饥饿模式下
  19.             if old&mutexStarving != 0 {
  20.                 // If this goroutine was woken and mutex is in starvation mode,
  21.                 // ownership was handed off to us but mutex is in somewhat
  22.                 // inconsistent state: mutexLocked is not set and we are still
  23.                 // accounted as waiter. Fix that.
  24.         // 若既没有已锁且正在尝试唤醒,或者等待队列为空,就代表产生了不一致的状态
  25.                 if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
  26.                     throw("sync: inconsistent mutex state")
  27.                 }
  28.         // 当前goroutine已经获取锁,等待队列减1;若等待者就一个,就切换正常模式。退出
  29.                 delta := int32(mutexLocked - 1<<mutexWaiterShift)
  30.                 if !starving || old>>mutexWaiterShift == 1 {
  31.                     delta -= mutexStarving
  32.                 }
  33.                 atomic.AddInt32(&m.state, delta)
  34.                 break
  35.             }
  36.       // 不处于饥饿模式下,设置当前goroutine为唤醒状态,重置自璇次数,继续尝试获取锁
  37.             awoke = true
  38.             iter = 0
  39.         } else {
  40.       // 若锁被其他goroutine占用了,就更新原状态,继续尝试获取锁
  41.             old = m.state
  42.         }
复制代码
考虑几种场景


  • 如果lock当前只有一个goroutine g1去获取锁,那么会直接快路径,cas更新已锁状态位,获取到锁
  • 如果锁已经被g1持有,

    • 此时g2会先自旋4次,
    • 然后计算期望状态为已锁、等待数量为1、唤醒状态位被清除
    • 在cas更新的时候尝试更新锁状态成功,接着因为原状态本身处于已锁,所以就不能获取到锁,只能排队,信号量堵塞
    • g1释放锁后,g2被唤醒,接着再次计算期望状态,并cas更新状态成功,直接获取到锁

  • 如果锁已经被g1持有,且g2在第一次尝试获取时超过了1ms(也就是饥饿阈值),那么

    • 计算期望状态为已锁、饥饿、清除唤醒状态位
    • cas更新状态成功,排在队列头,并被信号量堵塞
    • g1释放锁后,g2被唤醒就直接获取到锁,并减去排队数量以及清空饥饿位


释放锁


只有已锁——直接释放

如果没有排队的goroutine,没有处于饥饿状态,也没有真正尝试获取锁的goroutine,那么就可以直接cas更新状态为0
  1. func (m *Mutex) Unlock() {
  2.     // Fast path: drop lock bit.
  3.     new := atomic.AddInt32(&m.state, -mutexLocked)
  4.     if new != 0 {
  5.         // Outlined slow path to allow inlining the fast path.
  6.         // To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
  7.         m.unlockSlow(new)
  8.     }
  9. }
复制代码
慢释放


  • 如果原锁没有被锁住,就报错
  • 若原状态不处于饥饿状态,尝试唤醒等待者

    • 若现在锁已经被获取、正在获取、饥饿或者没有等待者,直接返回
    • 期望状态等待数量减1,并设置正在唤醒状态位
    • cas尝试更新期望状态,若成功,释放
    • 失败说明在这过程中又有goroutine在尝试获取,那么继续下一轮释放

  • 处于饥饿状态,直接释放信号量,移交锁所有权
  1. func (m *Mutex) unlockSlow(new int32) {
  2.   // 若原状态根本没有已锁状态位
  3.     if (new+mutexLocked)&mutexLocked == 0 {
  4.         throw("sync: unlock of unlocked mutex")
  5.     }
  6.   // 若原状态不处于饥饿状态
  7.     if new&mutexStarving == 0 {
  8.         old := new
  9.         for {
  10.       // 若没有等待,或者存在goroutine已经被唤醒,或者已经被锁住了,就不需要唤醒任何人,返回
  11.             if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
  12.                 return
  13.             }
  14.             // Grab the right to wake someone.
  15.       // 设置期望状态为正在获取状态位,并减去一个等待者
  16.             new = (old - 1<<mutexWaiterShift) | mutexWoken
  17.       // 尝试cas更新为期望新状态,若成功就释放信号量,失败就更新原状态,进行下一轮释放
  18.       // 失败说明在这过程中又有goroutine在尝试获取,比如已经获取到了、变成饥饿了、自旋等
  19.             if atomic.CompareAndSwapInt32(&m.state, old, new) {
  20.                 runtime_Semrelease(&m.sema, false, 1)
  21.                 return
  22.             }
  23.             old = m.state
  24.         }
  25.     } else {
  26.     // 饥饿模式下就移交锁所有权给下一个等待者,并放弃时间片,以便该等待者可以快速开始
  27.         runtime_Semrelease(&m.sema, true, 1)
  28.     }
  29. }
复制代码
到此这篇关于Golang Mutex实现互斥的具体方法的文章就介绍到这了,更多相关Golang Mutex互斥内容请搜索晓枫资讯以前的文章或继续浏览下面的相关文章希望大家以后多多支持晓枫资讯!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
18
积分
16
注册时间
2022-12-24
最后登录
2022-12-24

发表于 2024-9-8 16:50:24 | 显示全部楼层
感谢楼主,顶。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
18
积分
16
注册时间
2022-12-30
最后登录
2022-12-30

发表于 2024-10-4 10:19:14 | 显示全部楼层
路过,支持一下
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
12
积分
4
注册时间
2023-10-9
最后登录
2023-10-9

发表于 2025-3-2 01:54:57 | 显示全部楼层
顶顶更健康!!!
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
16
积分
12
注册时间
2022-12-29
最后登录
2022-12-29

发表于 2025-11-7 11:37:52 | 显示全部楼层
感谢楼主分享。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼
3楼
4楼
5楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表