经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » Go语言 » 查看文章
Go 互斥锁 Mutex 源码分析(二)
来源:cnblogs  作者:lubanseven  时间:2024/8/26 9:19:09  对本文有异议

原创文章,欢迎转载,转载请注明出处,谢谢。


0. 前言

Go 互斥锁 Mutex 源码分析(一) 一文中分析了互斥锁的结构和基本的抢占互斥锁的场景。在学习锁的过程中,看的不少文章是基于锁的状态解释的,个人经验来看,从锁的状态出发容易陷入细节,了解锁的状态转换过一段时间就忘,难以做到真正的理解。想来是用静态的方法分析动态的问题导致的。在实践中发现结合场景分析互斥锁对笔者来说更加清晰,因此有了 Go 互斥锁 Mutex 源码分析(一),本文接着结合不同场景分析互斥锁。

1. 不同场景下的锁状态

1.1 唤醒 goroutine

给出示意图:

image

G1 通过 Fast path 拿到锁,G2 在自旋之后,锁还是已锁状态。这是和 Go 互斥锁 Mutex 源码分析(一) 中的场景不一样的地方。接着自旋之后看,这种场景下会发生什么:

  1. func (m *Mutex) lockSlow() {
  2. ...
  3. for {
  4. if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
  5. ...
  6. }
  7. // step2: 当前锁未释放,old = 1
  8. new := old
  9. // step2: 如果当前锁是饥饿的,跳过期望状态 new 的更新
  10. // - 这里锁不是饥饿锁,new = old = 1
  11. if old&mutexStarving == 0 {
  12. new |= mutexLocked
  13. }
  14. // step2: 当前锁未释放,更新 new
  15. // - 更新 new 的等待 goroutine 位,表示有一个 goroutine 等待
  16. // - 更新 new 为 1001,new = 9
  17. if old&(mutexLocked|mutexStarving) != 0 {
  18. new += 1 << mutexWaiterShift
  19. }
  20. // step2: 当前 goroutine 不是饥饿状态,跳过 new 更新
  21. if starving && old&mutexLocked != 0 {
  22. new |= mutexStarving
  23. }
  24. // step2: 当前 goroutine 不是唤醒状态,跳过 new 更新
  25. if awoke {
  26. if new&mutexWoken == 0 {
  27. throw("sync: inconsistent mutex state")
  28. }
  29. new &^= mutexWoken
  30. }
  31. // step3: 原子 CAS 更新锁的状态
  32. // - 这里更新锁 m.state = 1 为 m.state = new = 9
  33. // - 表示当前有一个 goroutine 在等待锁
  34. if atomic.CompareAndSwapInt32(&m.state, old, new) {
  35. ...
  36. // waitStartTime = 0, queueLifo = false
  37. queueLifo := waitStartTime != 0
  38. if waitStartTime == 0 {
  39. // 更新 waitStartTime
  40. waitStartTime = runtime_nanotime()
  41. }
  42. // step4: 调用 runtime_SemacquireMutex 阻塞 goroutine
  43. runtime_SemacquireMutex(&m.sema, queueLifo, 1)
  44. starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
  45. ...
  46. }
  47. }
  48. }

Mutex.lockSlow 中更新了锁状态,接着进入 runtime_SemacquireMutexruntime_SemacquireMutex 是个非常重要的函数,我们有必要介绍它。

runtime_SemacquireMutex 接收三个参数。其中,重点是信号量 &m.semaqueueLifo。如果 queueLifo = false,当前 goroutine 将被添加到等待锁队列的队尾,阻塞等待唤醒。

G2 执行到 runtime_SemacquireMutex 时将进入阻塞等待唤醒状态,那么怎么唤醒 G2 呢? 我们需要看解锁过程。

1.1.1 sync.Mutex.Unlock

在 G2 阻塞等待唤醒时,G1 开始释放锁。进入 sync.Mutex.Unlock

  1. func (m *Mutex) Unlock() {
  2. ...
  3. // 将 m.state 的锁标志位置为 0,表示锁已释放
  4. new := atomic.AddInt32(&m.state, -mutexLocked)
  5. // 检查 new 是否为 0,如果为 0 则表示当前无 goroutine 等待,直接退出
  6. // 这里 new = 9,G2 在等待唤醒
  7. if new != 0 {
  8. m.unlockSlow(new)
  9. }
  10. }

进入 Mutex.unlockSlow

  1. func (m *Mutex) unlockSlow(new int32) {
  2. // 检查锁是否已释放,释放一个已经释放的锁将报错
  3. if (new+mutexLocked)&mutexLocked == 0 {
  4. fatal("sync: unlock of unlocked mutex")
  5. }
  6. // 检查锁是普通锁还是饥饿锁
  7. if new&mutexStarving == 0 {
  8. // 这里 new = 8 是普通锁,进入处理普通锁逻辑
  9. old := new
  10. for {
  11. // 如果没有 goroutine 等待,则返回
  12. if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
  13. return
  14. }
  15. // old 的唤醒位置 1,并且将等待的 goroutine 减 1,表示将唤醒一个等待中的 goroutine
  16. // 这里 new = 2
  17. new = (old - 1<<mutexWaiterShift) | mutexWoken
  18. // m.state = 8, old = 8, new = 2
  19. // CAS 更新 m.state = new = 2
  20. if atomic.CompareAndSwapInt32(&m.state, old, new) {
  21. // 进入 runtime_Semrelease 唤醒 goroutine
  22. runtime_Semrelease(&m.sema, false, 1)
  23. return
  24. }
  25. old = m.state
  26. }
  27. } else {
  28. // 处理饥饿锁逻辑,暂略
  29. runtime_Semrelease(&m.sema, true, 1)
  30. }
  31. }

sync.Mutex.Unlock 中的 runtime_Semrelease 唤醒队列中等待的 goroutine。其中,主要接收信号量 &m.semahandoff 两个参数。这里 handoff = false,将增加信号量,唤醒队列中等待的 goroutine G2。

1.1.2 唤醒 G2

唤醒之后,G2 继续执行后续代码:

  1. func (m *Mutex) lockSlow() {
  2. ...
  3. for {
  4. ...
  5. if atomic.CompareAndSwapInt32(&m.state, old, new) {
  6. ...
  7. runtime_SemacquireMutex(&m.sema, queueLifo, 1)
  8. // 检查唤醒的 goroutine 是否是饥饿模式
  9. // 如果是饥饿模式,或等待锁时间超过 1ms 则将 goroutine 置为饥饿模式
  10. // 注意这是 goroutine 是饥饿的,不是锁是饥饿锁
  11. starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
  12. // m.state 在 G1 unlock 时被更新为 2
  13. old = m.state
  14. // 锁不是饥饿锁,跳过
  15. if old&mutexStarving != 0 {
  16. ...
  17. }
  18. awoke = true
  19. iter = 0
  20. }
  21. }
  22. }

唤醒后的 G2 将 old 更新为 2。信号量增加,释放锁,只会唤醒一个 goroutine,被唤醒的 goroutine,这里是 G2,将继续循环:

  1. func (m *Mutex) lockSlow() {
  2. ...
  3. for {
  4. // old = 2,不会进入自旋
  5. if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
  6. ...
  7. }
  8. // 更新 new:new 是期望 goroutine 更新的状态
  9. // 这里 new = old = 2
  10. new := old
  11. // old = 2,不是饥饿锁
  12. // 更新 new 为 011,3
  13. if old&mutexStarving == 0 {
  14. new |= mutexLocked
  15. }
  16. // old = 2,表示锁已释放,不会将 goroutine 加入等待位
  17. if old&(mutexLocked|mutexStarving) != 0 {
  18. new += 1 << mutexWaiterShift
  19. }
  20. // 不饥饿,跳过
  21. if starving && old&mutexLocked != 0 {
  22. new |= mutexStarving
  23. }
  24. // awoke = true
  25. if awoke {
  26. if new&mutexWoken == 0 {
  27. throw("sync: inconsistent mutex state")
  28. }
  29. // 重置唤醒位,将 new 更新为 001,1
  30. new &^= mutexWoken
  31. }
  32. // m.state = 2, old = 2, new =1
  33. // CAS 更新 m.state= new = 1,表示当前 goroutine 已加锁
  34. if atomic.CompareAndSwapInt32(&m.state, old, new) {
  35. // 当前 goroutine 已加锁跳出循环
  36. if old&(mutexLocked|mutexStarving) == 0 {
  37. break // locked the mutex with CAS
  38. }
  39. ...
  40. }
  41. }
  42. }

在循环一轮后,G2 将拿到锁,接着执行临界区代码,最后在释放锁。

这里的场景是唤醒之后,goroutine 不饥饿。那么饥饿锁又是如何触发的呢?我们继续看饥饿锁的场景。

1.2 饥饿锁

饥饿锁场景下的示意图如下:

image

当 G1 释放锁时,G3 正在自旋等待锁释放。当 G1 释放锁时,被唤醒的 G2 和自旋的 G3 竞争大概率会拿不到锁。Go 在 1.9 中引入互斥锁的 饥饿模式 来确保互斥锁的公平性。

对于互斥锁循环中的大部分流程,我们在前两个场景下也过了一遍,这里有重点的摘写,以防赘述。

首先,还是看 G2,当 G1 释放锁时,G2 被唤醒,执行后续代码。如下:

  1. func (m *Mutex) lockSlow() {
  2. ...
  3. for {
  4. ...
  5. if atomic.CompareAndSwapInt32(&m.state, old, new) {
  6. ...
  7. runtime_SemacquireMutex(&m.sema, queueLifo, 1)
  8. // 唤醒 G2,G2 等待锁时间超过 1ms
  9. // starving = true
  10. starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
  11. // 锁被 G3 抢占,m.state = 0011
  12. old = m.state
  13. // 这时候 old 还不是饥饿锁,跳过
  14. if old&mutexStarving != 0 {
  15. ...
  16. }
  17. awoke = true
  18. iter = 0
  19. }
  20. }
  21. }

唤醒 G2 之后,G2 等待锁时间超过 1ms 进入饥饿模式。接着进入下一轮循环:

  1. func (m *Mutex) lockSlow() {
  2. ...
  3. for {
  4. // old 是唤醒锁,不会进入自旋
  5. if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
  6. ...
  7. }
  8. // 锁的期望状态,new = old = 0011
  9. new := old
  10. // 锁不是饥饿锁,更新 new 的锁标志位为已锁
  11. // new = 0011
  12. if old&mutexStarving == 0 {
  13. new |= mutexLocked
  14. }
  15. // 锁如果是饥饿或者已锁状态更新 goroutine 等待位
  16. // new = 1011
  17. if old&(mutexLocked|mutexStarving) != 0 {
  18. new += 1 << mutexWaiterShift
  19. }
  20. // goroutine 饥饿,且锁已锁
  21. // 更新 new 为饥饿状态,new = 1111
  22. if starving && old&mutexLocked != 0 {
  23. new |= mutexStarving
  24. }
  25. // 这里 G2 是唤醒的,重置唤醒位
  26. // new = 1101
  27. if awoke {
  28. if new&mutexWoken == 0 {
  29. throw("sync: inconsistent mutex state")
  30. }
  31. new &^= mutexWoken
  32. }
  33. // CAS 更新 m.state = new = 1101
  34. if atomic.CompareAndSwapInt32(&m.state, old, new) {
  35. ...
  36. // G2 入队列过,这里 queueLifo = true
  37. queueLifo := waitStartTime != 0
  38. // 将 G2 重新加入队列,并加入到队首,阻塞等待
  39. runtime_SemacquireMutex(&m.sema, queueLifo, 1)
  40. ...
  41. }
  42. }
  43. }

G2 进入饥饿模式,将互斥锁置为饥饿模式,当前互斥锁状态为 m.state = 1101。G2 作为队列中的队头,阻塞等待锁释放。

类似的,我们看 G3 释放锁的过程。

1.2.1 释放饥饿锁

G3 开始释放锁:

  1. func (m *Mutex) Unlock() {
  2. ...
  3. // new = 1100
  4. new := atomic.AddInt32(&m.state, -mutexLocked)
  5. if new != 0 {
  6. // 进入 Mutex.unlockSlow
  7. m.unlockSlow(new)
  8. }
  9. }
  10. func (m *Mutex) unlockSlow(new int32) {
  11. ...
  12. // new = 1100,是饥饿锁
  13. if new&mutexStarving == 0 {
  14. ...
  15. } else {
  16. // 进入处理饥饿锁逻辑
  17. // handoff = true,直接将队头阻塞的 goroutine 唤醒
  18. runtime_Semrelease(&m.sema, true, 1)
  19. }
  20. }

1.2.2 饥饿锁唤醒

在一次的在队头中阻塞的 G2 被唤醒,接着执行唤醒后的代码:

  1. func (m *Mutex) lockSlow() {
  2. ...
  3. for {
  4. ...
  5. if atomic.CompareAndSwapInt32(&m.state, old, new) {
  6. ...
  7. runtime_SemacquireMutex(&m.sema, queueLifo, 1)
  8. starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
  9. old = m.state
  10. // old = 1100,是饥饿锁
  11. if old&mutexStarving != 0 {
  12. ...
  13. // delta = -(1001)
  14. delta := int32(mutexLocked - 1<<mutexWaiterShift)
  15. if !starving || old>>mutexWaiterShift == 1 {
  16. ...
  17. // delta = -(1101)
  18. delta -= mutexStarving
  19. }
  20. //更新互斥锁状态 m.state = 0001,退出循环
  21. atomic.AddInt32(&m.state, delta)
  22. break
  23. }
  24. }
  25. }
  26. }

唤醒之后的 G2 直接获得锁,将互斥锁状态置为已锁,直到释放。

2. 锁状态流程

前面我们根据几个场景给出了互斥锁的状态转换过程,这里直接给出互斥锁的流程图如下:

image

3. 总结

本文是 Go 互斥锁 Mutex 源码分析的第二篇,进一步通过两个场景分析互斥锁的状态转换。互斥锁的状态转换如果陷入状态更新,很容易头晕,这里通过不同场景,逐步分析,整个状态,接着给出状态转换流程图,力图做到源码层面了解锁的状态转换。


原文链接:https://www.cnblogs.com/xingzheanan/p/18377669

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号