Go 互斥锁 Mutex 源码分析 (一)
0. 前言
锁作为并发编程中的关键一环,是应该要深入掌握的。
1. 锁
1.1 示例
实现锁很简单,示例如下:
var global int
func main() {
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
mu.Lock()
global++
mu.Unlock()
}(i)
}
wg.Wait()
fmt.Println(global)
}
输出:
2
在 goroutine 中给全局变量 global 加锁,实现并发顺序增加变量。其中,sync.Mutex.Lock() 对变量/临界区加锁,sync.Mutex.Unlock() 对变量/临界区解锁。
1.2 sync.Mutex
我们看 sync.Mutex 互斥锁结构:
type Mutex struct {
state int32
sema uint32
}
其中,state 表示锁的状态,sema 表示信号量。
进入 sync.Mutex.Lock() 查看加锁的方法。
1.2.1 sync.Mutex.Lock()
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// Slow path (outlined so that the fast path can be inlined)
m.lockSlow()
}
首先进入 Fast path 逻辑,原子 CAS 操作比较锁状态 m.state 和 0,如果相等则更新当前锁为已加锁状态。这里锁标志位如下:

从低(右)到高(左)的三位表示锁状态/唤醒状态/饥饿状态:
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
)
标志位初始值为 0,1 表示状态生效。
前三位之后的位数表示排队等待锁的 goroutine 数目,总共可以允许 1<<(32-3) 个 goroutine 等待锁。
这里假设有两个 goroutine G1 和 G2 抢占锁,其中 G1 通过 Fast path 获取锁,将锁的状态置为 1。这时候 G2 未获得锁,进入 Slow path:
func (m *Mutex) lockSlow() {
var waitStartTime int64
starving := false
awoke := false
iter := 0
old := m.state
for {
// step1: 进入自旋
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin()
iter++
old = m.state
continue
}
...
}
}
Slow path 的代码量不大,但涉及状态转换很复杂,不容易看懂。这里拆成每个步骤,根据不同场景分析具体源码。
进入 Mutex.lockSlow(),初始化各个状态位,将当前锁状态赋给变量 old,进入 for 循环,执行第一步自旋逻辑。自旋会独占 CPU,让 CPU 空跑,但是减少了频繁切换 goroutine 带来的内存/时间消耗。如果使用的适当,会节省 CPU 开销,使用的不适当,会造成 CPU 浪费。这里进入自旋是很严苛的,通过三个条件判断能否自旋:
- 当前锁是普通模式才能进入自旋。
- runtime.sync_runtime_canSpin 需要返回 true:
- 当前 goroutine 进入自旋的次数小于 4 次;
- goroutine 运行在多 CPU 的机器上;
- 当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空;
假设 G2 可以进入自旋,运行 runtime_doSpin():
# src/runtime/lock_futex.go
const active_spin_cnt = 30
# src/runtime/proc.go
//go:linkname sync_runtime_doSpin sync.runtime_doSpin
//go:nosplit
func sync_runtime_doSpin() {
procyield(active_spin_cnt)
}
# src/runtime/asm_amd64.s
TEXT runtime·procyield(SB),NOSPLIT,$0-0
MOVL cycles+0(FP), AX
again:
PAUSE
SUBL $1, AX
JNZ again
RET
自旋实际上是 CPU 执行了 30 次 PAUSE 指令。
自旋是在等待,等待锁释放的过程。假设在自旋期间 G1 已释放锁,更新 m.state 为 0。那么,在 G2 自旋逻辑中 old = m.state 将更新 old 为 0。继续往下看,for 循环中做了什么。
func (m *Mutex) lockSlow() {
...
for {
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
...
}
// step2: 更新 new,这里 new 为 0
new := old
// step2: 继续更新 new
// - 如果锁为普通锁,更新锁状态为已锁。如果锁为饥饿锁,跳过饥饿锁更新。
// - 这里更新锁为 1
if old&mutexStarving == 0 {
new |= mutexLocked
}
// step2:继续更新 new
// - 如果锁为已锁或饥饿的任何一种,则更新 new 的 goroutine 排队等待位
// - 这里锁为已释放,new 为 1
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// step2: 继续更新 new
// - 如果 goroutine 处于饥饿状态,并且当前锁是已锁的,更新 new 为饥饿状态
// - 这里锁为已释放,new 为 1
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
// step2: 继续更新 new
// - 如果当前 goroutine 是唤醒的,重置唤醒位为 0
// - goroutine 不是唤醒的,new 为 1
if awoke {
// The goroutine has been woken from sleep,
// so we need to reset the flag in either case.
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
new &^= mutexWoken
}
// step3: CAS 比较 m.state 和 old,如果一致则更新 m.state 到 new
// - 这里 m.state = 0,old = 0,new = 1
// - 更新 m.state 为 new,当前 goroutine 获得锁
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 如果更新锁之前的状态不是饥饿或已锁,表示当前 goroutine 已获得锁,跳出循环。
if old&(mutexLocked|mutexStarving) == 0 {
break // locked the mutex with CAS
}
...
}
}
}
这里将自旋后的逻辑简化为两步,更新锁的期望状态 new 和通过原子 CAS 操作更新锁。这里的场景不难,我们可以简化上述流程为如下示意图:

2. 小结
本文介绍了 Go 互斥锁的基本结构,并且给出一个抢占互斥锁的基本场景,通过场景从源码角度分析互斥锁。
Go 互斥锁 Mutex 源码分析 (一)的更多相关文章
- Golang 读写锁RWMutex 互斥锁Mutex 源码详解
前言 Golang中有两种类型的锁,Mutex (互斥锁)和RWMutex(读写锁)对于这两种锁的使用这里就不多说了,本文主要侧重于从源码的角度分析这两种锁的具体实现. 引子问题 我一般喜欢带着问题去 ...
- concurrent(三)互斥锁ReentrantLock & 源码分析
参考文档:Java多线程系列--“JUC锁”02之 互斥锁ReentrantLock:http://www.cnblogs.com/skywang12345/p/3496101.html Reentr ...
- ReentrantLock 锁释放源码分析
ReentrantLock 锁释放源码分析: 调用的是unlock 的方法: public void unlock() { sync.release(1); } 接下来分析release() 方法: ...
- [转]分布式锁-RedisLockRegistry源码分析
前言 官网的英文介绍大概如下: Starting with version 4.0, the RedisLockRegistry is available. Certain components (f ...
- ReentrantLock(重入锁)简单源码分析
1.ReentrantLock是基于AQS实现的一种重入锁. 2.先介绍下公平锁/非公平锁 公平锁 公平锁是指多个线程按照申请锁的顺序来获取锁. 非公平锁 非公平锁是指多个线程获取锁的顺序并不是按照申 ...
- Laravel Redis分布式锁实现源码分析
首先是锁的抽象类,定义了继承的类必须实现加锁.释放锁.返回锁拥有者的方法. namespace Illuminate\Cache; abstract class Lock implements Loc ...
- 【协作式原创】查漏补缺之Golang中mutex源码实现
概览最简单版的mutex(go1.3版本) 预备知识 主要结构体 type Mutex struct { state int32 // 指代mutex锁当前的状态 sema uint32 // 信号量 ...
- go中sync.Mutex源码解读
互斥锁 前言 什么是sync.Mutex 分析下源码 Lock 位运算 Unlock 总结 参考 互斥锁 前言 本次的代码是基于go version go1.13.15 darwin/amd64 什么 ...
- 源码分析:Semaphore之信号量
简介 Semaphore 又名计数信号量,从概念上来讲,信号量初始并维护一定数量的许可证,使用之前先要先获得一个许可,用完之后再释放一个许可.信号量通常用于限制线程的数量来控制访问某些资源,从而达到单 ...
- 死磕 java集合之ConcurrentHashMap源码分析(一)
开篇问题 (1)ConcurrentHashMap与HashMap的数据结构是否一样? (2)HashMap在多线程环境下何时会出现并发安全问题? (3)ConcurrentHashMap是怎么解决并 ...
随机推荐
- 浅谈性能测试稳定性 Constant Throughput Timer(常数吞吐量定时器)
在性能测试过程中总会收到一些需求如:单接口每秒并发20,这种并发持续60秒,通过负载测试查看系统稳定性,今天就让我们来浅谈一下这种场景如何去实现性能测试~ 这种场景可以用两种方法去实现: 一.我们通过 ...
- 七牛云 + PicGo
下载PicGo https://github.com/Molunerfinn/PicGo/releases/tag/v2.3.1 七牛云配置 1.AccessKey和SecretKey:可以在七牛云控 ...
- Apifox 6月更新|定时任务、内网自部署服务器运行接口定时导入、数据库 SSH 隧道连接
Apifox 新版本上线啦!!! 看看本次版本更新主要涵盖的重点内容,有没有你所关注的功能特性: 自动化测试支持设置「定时任务」 支持内网自部署服务器运行「定时导入」 数据库均支持通过 SSH 隧道 ...
- Vue 框架怎么实现对象和数组的监听?
如果被问到 Vue 怎么实现数据双向绑定,大家肯定都会回答 通过 Object.defineProperty() 对数据进行劫持,但是 Object.defineProperty() 只能对属性进行数 ...
- Mysql中where条件自动类型转换的坑
我有张表,其主键id字段为varchar(5),内容是5位随机不重复字符串表的内容大概是这样的 id name s8bk2 admin 9f0ps username 在一个方法中我查询了这张表,代码大 ...
- webpack4.15.1 学习笔记(五) — 生产环境构建
目录 生产环境构建 指定环境 生产环境构建 development和production的构建目标差异很大.dev中,需要具有实时重新加载或HMR能力的 source map 和 server.而在p ...
- 背景色透明度兼容IE8的写法
本文为Echoyya.所创,转载请带上原文链接,感谢 https://www.cnblogs.com/echoyya/p/14236242.html 通常的做法 目前大多数浏览器都支持 CSS3,只需 ...
- API引用在Element UI (Vue 2)和Element Plus (Vue 3)中的不同
API 变动 样式类名变化: 一些组件的样式类名有所变动,可能需要更新你的自定义样式. 事件名和属性名变化: 某些组件的事件名和属性名发生了变化,需要检查 Element Plus 文档 以了解详细信 ...
- Unity入门学习日记(一)
UGUI的初步使用 1. Canvas 使用UI的时候,所有的UI元素都作为Canvas的子节点存在于Canvas中,如果创建UI元素时没有Canvas作为父节点,会自动生成一个Canvas,是一位& ...
- 记一次seata启动错误日志ErrMsg:failed to req API:/nacos/v1/ns/instance after all servers....
错误日志如下: java.lang.RuntimeException: ErrCode:500, ErrMsg:failed to req API:/nacos/v1/ns/instance afte ...