Mutex是一个互斥锁,可以创建为其他结构体的字段;零值为解锁状态。Mutex类型的锁和线程无关,可以由不同的线程加锁和解锁。
互斥锁的作用是保证共享资源同一时刻只能被一个 Goroutine 占用,一个 Goroutine 占用了,其他的 Goroutine 则阻塞等待。

1、数据结构

type Mutex struct {
state int32 // 表示当前互斥锁的状态
sema uint32 // 信号量变量,用来控制等待 goroutine 的阻塞休眠和唤醒
}

基于该数据结构,实现了两种方法,加锁、释放锁

type Locker interface {
Lock()
Unlock()
}

const (
mutexLocked = 1 << iota // 表示锁是否可用(0可用,1被别的goroutine占用),001
mutexWoken // 表示mutex是否被唤醒,010
mutexStarving // 当前的互斥锁进入饥饿状态,100
mutexWaiterShift = iota // 表示统计阻塞在该mutex上的goroutine数目需要移位的数值,1<<(32-3)个
)
// sema + 1,挂起 goroutine
// 1.不断调用尝试获取锁
// 2.休眠当前 goroutine
// 3.等待信号量,唤醒 goroutine
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
// sema - 1,唤醒 sema 上等待的一个 goroutine
runtime_Semrelease(&m.sema, false, 1)

2、模式

2.1、正常模式

在正常模式下,等待的 goroutine 会按照先进先出的顺序得到锁。刚被唤醒的 goroutine 与新创建的 goroutine 竞争时,大概率无法获得锁,如 G1和 G2 竞争,此时 G1 已经占着 CPU 了,所以大概率拿到锁。

如果 goroutine 超过 1ms,没有获取锁,就会将当前锁切换为饥饿模式。

2.2、饥饿模式

避免 goroutine 被饿死,1.19 引入了饥饿模式

在饥饿模式下,互斥锁会直接交给等到队列最前面的 goroutine,新的 goroutine 在该状态下不能获取锁,也不能进入自旋,只能在队列末尾等待。

2.3、状态切换

正常模式下,

如果队列中只剩一个goroutine 获得了互斥锁或者它等待的时间少于 1ms,那么就会切换到正常模式。

3、加锁

1、Fast path

// 如果锁没被占用,也不是饥饿状态,也没有唤醒goroutine,也没有等待goroutine,加锁成功
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}

加锁的时候先通过一次 CAS(Compare And Swap) 看能不能拿到锁,如果拿到,直接返回。

// 先判断参数addr指向的被操作值与参数old的值是否相等
// 如果相等,会用参数new代表的新值替换掉原先的旧值,否则 false
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

2、Slow path

如果状态不是 0 ,就会尝试通过自旋等方式等待锁释放,大致分为:

  1. 判断当前 goroutine 能否进入自旋
  2. 通过自旋等待互斥锁的释放
  3. 计算互斥锁的最新状态
  4. 更新互斥锁的状态并获取锁
// 等待时间
var waitStartTime int64
// 饥饿标记
starving := false
// 唤醒标记
awoke := false
// 自旋次数
iter := 0
// 当前的锁的状态
old := m.state
for {
// 步骤一
// 如果锁是正常状态,锁还没被释放,就自旋
// 因为饥饿模式下,需要保证等到队列中的 goroutine 能够获得锁的的所有权,防止等待队列饿死
// 如果锁在饥饿模式或已经解锁,或不符合自旋条件就结束自旋
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// 如果等待队列有 goroutine ,锁没有设置唤醒状态,就设置为唤醒
// 用来,当锁解锁时,不会去唤醒已经阻塞的 goroutine,保证自己更大概率拿到锁
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
// 自旋
runtime_doSpin()
// 自旋次数加1
iter++
// 设置当前锁的状态
old = m.state
continue
}
------------------------------------------------------------------------------>
// 步骤二
// 此时可能锁变为饥饿状态或者已经解锁了,或者不符合自旋条件
// 获取锁最新状态
new := old // 如果当前是正常模式,尝试加锁。
// 饥饿状态下要让出竞争权利,不能加锁
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 如果当前被锁定或者处于饥饿模式,把自己放到等待队列,waiter加一,表示等待一个等待计数
// 这块的状态,goroutine 只能等着,饥饿状态要让出竞争权利
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
} // 如果已经是饥饿状态,starving为真,并且old 的锁是占用情况,更新状态改为饥饿状态
if starving && old&mutexLocked != 0 {
new |= mutexStarving
} // 如果awoke在上面自旋时设置成功,那么在这要消除标志位
// 因为该 goroutine 要么获得了锁,要么进入休眠,和唤醒状态没啥关系
// 后续流程会导致当前线程被挂起,需要等待其他释放锁的 goroutine 唤醒,
// 如果 unlock 是发现mutexWoken不是 0,就不会去唤醒
if awoke {
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
// 清除唤醒标志位
new &^= mutexWoken
}
------------------------------------------------------------------------------>
// 步骤三
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 1.如果原来状态没有上锁,也没有饥饿,那么直接返回,表示获取到锁
if old&(mutexLocked|mutexStarving) == 0 {
break // locked the mutex with CAS
} // 2.到这里是没有获取到锁,判断一下等待时长是否不为0
// 如果新的 goroutine 来抢占锁,会返回 false
// 如果不是新的,那么加入到队列头部
// 保证等待最久的 goroutine 优先拿到锁
queueLifo := waitStartTime != 0 // 3.如果等待时间为0,那么初始化等待时间
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
// 如果不等于,说明不是第一次来,是被唤醒后过来的,则加入队列头部,queueLifo=true // 4.阻塞等待,sema+1,并挂起 goroutine,
// 如果后面 goroutine 被唤醒,就从该位置往下执行
runtime_SemacquireMutex(&m.sema, queueLifo, 1) // 5.说明该 goroutine 被唤醒
// 判断该 goroutine 是否长时间没有获得锁,如果是,就是饥饿的 goroutine
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
// 被挂起的时间有点长,需要重新获取一下当前锁的状态
old = m.state // 6.判断是否已经处于饥饿状态,处于,直接获得锁,如果不处于直接跳出
// 饥饿状态下,被唤醒的协程直接获得锁。
if old&mutexStarving != 0 {
// 饥饿状态下,被唤醒,发现锁没释放,唤醒值是 1,等待列表没有,报错
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
delta := int32(mutexLocked - 1<<mutexWaiterShift) // 7.如果唤醒等待队列的 goroutine 不饥饿,或是等待队列中的最后一个 goroutine
if !starving || old>>mutexWaiterShift == 1 {
// 就从饥饿模式切换会正常模式
delta -= mutexStarving
} // 9.设置状态
// 将锁状态设置为等待数量减1,同时设置为锁定,加锁成功
atomic.AddInt32(&m.state, delta)
break
}
// 当前 goroutine 是被系统唤醒的
awoke = true
// 重置自旋次数
iter = 0
} else {
// 如果 CAS 失败,重新开始
old = m.state
}
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}

3、小结

4、自旋

自旋是一种多线程同步机制,当前的进程在进入自旋的过程中会一直保持 CPU 的占用,持续检查某个条件是否为真。

4.1、canSpin

runtime_canSpin(iter)
  • CPU核数要大于1,否则自旋没有意义,因为此时不可能有其他协程释放锁
  • 当前Goroutine为了获取该锁进入自旋的次数 iter 小于四次
  • 当前机器上至少存在一个正在运行 Process
  • 处理的运行 G 队列为空,否则会延迟调度

它的实现方法链接到了sync_runtime_canSpin

4.2、doSpin

runtime_doSpin()

func sync_runtime_doSpin() {
procyield(active_spin_cnt)
} TEXT runtime·procyield(SB),NOSPLIT,$0-0
MOVL cycles+0(FP), AX
again:
PAUSE
SUBL $1, AX
JNZ again
RET

它的实现方法链接到了 sync_runtime_doSpin

会执行 30 次 PAUSE指令,每执行一次再检查是否可以加锁,循环进行。该过程中,进程仍是执行状态

4.3、优势

更充分的利用CPU,尽量避免 goroutine 切换。因为当前申请加锁的 goroutine 拥有CPU,如果经过短时间的自旋可以获得锁,当前协程可以继续运行,不必进入阻塞状态。

对于新来进程一直进行自旋加锁,排队中的进程长时间无法拿到锁,则设置饥饿状态,该状态下不允许自旋。

5、小结

  1. 上来先一个 CAS ,如果锁正空闲,并且没人抢,那么加锁成功;
  2. 否则,自旋几次,如果成功,也不用加入队列;
  3. 否则,加入队列;
  4. 从队列中被唤醒:
    1. 正常模式:和新来的一起抢锁,大概率失败
    2. 饥饿模式:肯定拿到锁

Go 互斥锁Mutex的更多相关文章

  1. 互斥锁Mutex与信号量Semaphore的区别

    转自互斥锁Mutex与信号量Semaphore的区别 多线程编程中,常常会遇到这两个概念:Mutex和Semaphore,两者之间区别如下: 有人做过如下类比: Mutex是一把钥匙,一个人拿了就可进 ...

  2. 深入理解Solaris内核中互斥锁(mutex)与条件变量(condvar)之协同工作原理

    在Solaris上写内核模块总是会用到互斥锁(mutex)与条件变量(condvar), 光阴荏苒日月如梭弹指一挥间,Solaris的大船说沉就要沉了,此刻心情不是太好(Orz).每次被年轻的有才华的 ...

  3. Linux内核互斥锁--mutex

    一.定义: /linux/include/linux/mutex.h   二.作用及访问规则: 互斥锁主要用于实现内核中的互斥访问功能.内核互斥锁是在原子 API 之上实现的,但这对于内核用户是不可见 ...

  4. 线程锁(互斥锁Mutex)及递归锁

    一.线程锁(互斥锁) 在一个程序内,主进程可以启动很多个线程,这些线程都可以访问主进程的内存空间,在Python中虽然有了GIL,同一时间只有一个线程在运行,可是这些线程的调度都归系统,操作系统有自身 ...

  5. 线程锁(互斥锁Mutex)

    线程锁(互斥锁Mutex) 一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况? # -*- cod ...

  6. Golang 读写锁RWMutex 互斥锁Mutex 源码详解

    前言 Golang中有两种类型的锁,Mutex (互斥锁)和RWMutex(读写锁)对于这两种锁的使用这里就不多说了,本文主要侧重于从源码的角度分析这两种锁的具体实现. 引子问题 我一般喜欢带着问题去 ...

  7. 一文带你剖析LiteOS互斥锁Mutex源代码

    摘要:多任务环境下会存在多个任务访问同一公共资源的场景,而有些公共资源是非共享的临界资源,只能被独占使用.LiteOS使用互斥锁来避免这种冲突,互斥锁是一种特殊的二值性信号量,用于实现对临界资源的独占 ...

  8. [Go] golang互斥锁mutex

    1.互斥锁用于在代码上创建一个临界区,保证同一时间只有一个goroutine可以执行这个临界区代码2.Lock()和Unlock()定义临界区 package main import ( " ...

  9. Go基础系列:互斥锁Mutex和读写锁RWMutex用法详述

    sync.Mutex Go中使用sync.Mutex类型实现mutex(排他锁.互斥锁).在源代码的sync/mutex.go文件中,有如下定义: // A Mutex is a mutual exc ...

  10. go互斥锁Mutex

    go mutex是互斥锁,只有Lock和Unlock两个方法,在这两个方法之间的代码不能被多个goroutins同时调用到. 看代码: package main import ( "fmt& ...

随机推荐

  1. js Fetch返回数据res.json()报错问题

    前言 一直以来在简单的场景中经常使用fetch代替第三方请求库, fetch是JavaScript的原生函数, 简单.高效.快速.稳定.可定制等等诸多优点.一直也是用着很是舒服,直到有一天它竟然报错了 ...

  2. Python基础之dict和set的使用

    dict Python内置了字典:dict的支持,dict全称dictionary,在其他语言种也称为map,使用键-值(key-value)存储,具有极快的查找速度. 举个例子,假设要根据同学的名字 ...

  3. 从 Airflow 到 Apache DolphinScheduler,有赞大数据开发平台的调度系统演进

    点击上方 蓝字关注我们 作者 | 宋哲琦 ✎ 编 者 按 在不久前的 Apache  DolphinScheduler Meetup 2021 上,有赞大数据开发平台负责人 宋哲琦 带来了平台调度系统 ...

  4. Apache DolphinScheduler之最美好的遇见

    关于 Apache DolphinScheduler社区 Apache DolphinScheduler(incubator) 于17年在易观数科立项,19年3月开源, 19 年8月进入Apache ...

  5. [题解]Balance

    1.题目 POJ-1837 2.题目大意 一个天平上有一些钩子,现在有一些砝码.给出每个钩子到原点(姑且这么叫吧)的距离(-15 ~ 15,负数代表在左边,正数相反)以及砝码的重量(1 ~ 20),求 ...

  6. Word 分页符怎么使用

    当一页内容输入完之后,还留有很多空白区域没有填写,一直按回车键跳转到下一页显得复杂,并且回车键经过的地方都是段落. 可以手动添加分页符,使当前页跳转到下一页. 也可以使用快捷键Ctrl + Enter ...

  7. Ubuntu20.04配置 ES7.17.0集群

    Ubuntu20.04配置 ES7.17.0集群 ES能做什么? elasticsearch简写es,es是一个高扩展.开源的全文检索和分析引擎,它可以准实时地快速存储.搜索.分析海量的数据. Ubu ...

  8. 对Github指定类目的内容进行监控和推送

    很久之前看到HACK学习呀有一个Github 安全搬运工的系列文章,个人觉得很不错,想要在自己的公众号上也做这方面的内容,内容的编辑排版相对来说比较容易,这样问题就回归到Github安全内容的获取上 ...

  9. Springboot mybatis总结

    mybatis 总结 属性配置 1. mybatis.configuration.mapUnderscoreToCamelCase=true mapUnderscoreToCamelCase用于映射表 ...

  10. Hive的基本知识与操作

    Hive的基本知识与操作 目录 Hive的基本知识与操作 Hive的基本概念 为什么使用Hive? Hive的特点: Hive的优缺点: Hive应用场景 Hive架构 Client Metastor ...