## 1 Mutex数据结构
### 1.1 Mutex结构体
源码包`src/sync/mutex.go:Mutex`定义了互斥锁的数据结构:
~~~go
type Mutex struct {
state int32
sema uint32
}
~~~
* Mutex.state表示互斥锁的状态,比如是否被锁定等。
* Mutex.sema表示信号量,协程阻塞等待该信号量,解锁的协程释放信号量从而唤醒等待信号量的协程。
我们看到Mutex.state是32位的整型变量,内部实现时把该变量分成四份,用于记录Mutex的四种状态。
下图展示Mutex的内存布局:
![](https://img.kancloud.cn/62/53/62537e743f76c0f256c36eea0d89841c_735x237.png)
* Locked: 表示该Mutex是否已被锁定,0:没有锁定 1:已被锁定。
* Woken: 表示是否有协程已被唤醒,0:没有协程唤醒 1:已有协程唤醒,正在加锁过程中。
* Starving:表示该Mutex是否处于饥饿状态,0:没有饥饿 1:饥饿状态,说明有协程阻塞了超过1ms。
* Waiter: 表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量。
协程之间抢锁实际上是抢给Locked赋值的权利,能给Locked域置1,就说明抢锁成功。抢不到的话就阻塞等待Mutex.sema信号量,一旦持有锁的协程解锁,等待的协程会依次被唤醒。
Woken和Starving主要用于控制协程间的抢锁过程,后面再进行了解。
### 1.2 Mutex方法
Mutex对外提供两个方法,实际上也只有这两个方法:
* Lock() : 加锁方法
* Unlock(): 解锁方法
下面我们分析一下加锁和解锁的过程,加锁分成功和失败两种情况,成功的话直接获取锁,失败后当前协程被阻塞,同样,解锁时根据是否有阻塞协程也有两种处理。
## 3 加解锁过程
## 3.1 简单加锁
假定当前只有一个协程在加锁,没有其他协程干扰,那么过程如下图所示:
![](https://img.kancloud.cn/b0/29/b02992c18b74569eef78f4de76e5e09e_841x307.png)
加锁过程会去判断Locked标志位是否为0,如果是0则把Locked位置1,代表加锁成功。从上图可见,加锁成功后,只是Locked位置1,其他状态位没发生变化
## 3.2 加锁被阻塞
假定加锁时,锁已被其他协程占用了,此时加锁过程如下图所示:
![](https://img.kancloud.cn/32/d3/32d300a03fc3b58b0f4552d6bd3d0938_841x457.png)
从上图可看到,当协程B对一个已被占用的锁再次加锁时,Waiter计数器增加了1,此时协程B将被阻塞,直到Locked值变为0后才会被唤醒。
## 3.3 简单解锁
假定解锁时,没有其他协程阻塞,此时解锁过程如下图所示:
![](https://img.kancloud.cn/91/72/91725a70081dffd1091bd94bca494ee0_841x457.png)
由于没有其他协程阻塞等待加锁,所以此时解锁时只需要把Locked位置为0即可,不需要释放信号量
## 3.4 解锁并唤醒协程
假定解锁时,有1个或多个协程阻塞,此时解锁过程如下图所示:
![](https://img.kancloud.cn/59/b9/59b900033000d67da9ed3c071143d897_841x737.png)
协程A解锁过程分为两个步骤,一是把Locked位置0,二是查看到Waiter>0,所以释放一个信号量,唤醒一个阻塞的协程,被唤醒的协程B把Locked位置1,于是协程B获得锁。
## 4 自旋过程
加锁时,如果当前Locked位为1,说明该锁当前由其他协程持有,尝试加锁的协程并不是马上转入阻塞,而是会持续的探测Locked位是否变为0,这个过程即为自旋过程。
自旋时间很短,但如果在自旋过程中发现锁已被释放,那么协程可以立即获取锁。此时即便有协程被唤醒也无法获取锁,只能再次阻塞。
自旋的好处是,当加锁失败时不必立即转入阻塞,有一定机会获取到锁,这样可以避免协程的切换。
## 4.1 什么是自旋
自旋对应于CPU的”PAUSE”指令,CPU对该指令什么都不做,相当于CPU空转,对程序而言相当于sleep了一小段时间,时间非常短,当前实现是30个时钟周期。
自旋过程中会持续探测Locked是否变为0,连续两次探测间隔就是执行这些PAUSE指令,它不同于sleep,不需要将协程转为睡眠状态。
## 4.2 自旋条件
加锁时程序会自动判断是否可以自旋,无限制的自旋将会给CPU带来巨大压力,所以判断是否可以自旋就很重要了。
自旋必须满足以下所有条件:
* 自旋次数要足够小,通常为4,即自旋最多4次
* CPU核数要大于1,否则自旋没有意义,因为此时不可能有其他协程释放锁
* 协程调度机制中的Process数量要大于1,比如使用GOMAXPROCS()将处理器设置为1就不能启用自旋
* 协程调度机制中的可运行队列必须为空,否则会延迟协程调度
可见,自旋的条件是很苛刻的,总而言之就是不忙的时候才会启用自旋。
## 4.3 自旋的优势
自旋的优势是更充分的利用CPU,尽量避免协程切换。因为当前申请加锁的协程拥有CPU,如果经过短时间的自旋可以获得锁,当前协程可以继续运行,不必进入阻塞状态。
## 4.4 自旋的问题
如果自旋过程中获得锁,那么之前被阻塞的协程将无法获得锁,如果加锁的协程特别多,每次都通过自旋获得锁,那么之前被阻塞的进程将很难获得锁,从而进入饥饿状态。
为了避免协程长时间无法获取锁,自1.8版本以来增加了一个状态,即Mutex的Starving状态。这个状态下不会自旋,一旦有协程释放锁,那么一定会唤醒一个协程并成功加锁。
## 5 Woken状态
Woken状态用于加锁和解锁过程的通信,举个例子,同一时刻,两个协程一个在加锁,一个在解锁,在加锁的协程可能在自旋过程中,此时把Woken标记为1,用于通知解锁协程不必释放信号量了,好比在说:你只管解锁好了,不必释放信号量,我马上就拿到锁了。
## 6为什么重复解锁要panic
可能你会想,为什么Go不能实现得更健壮些,多次执行Unlock()也不要panic?
仔细想想Unlock的逻辑就可以理解,这实际上很难做到。Unlock过程分为将Locked置为0,然后判断Waiter值,如果值>0,则释放信号量。
如果多次Unlock(),那么可能每次都释放一个信号量,这样会唤醒多个协程,多个协程唤醒后会继续在Lock()的逻辑里抢锁,势必会增加Lock()实现的复杂度,也会引起不必要的协程切换。
- 概述
- go语言基础特性
- Go语言声明
- Go项目构建及编译
- go command
- 程序设计原则
- Go基础
- 变量
- 常量
- iota
- 基本类型
- byte和rune类型
- 类型定义和类型别名
- 数组
- string
- 高效字符串连接
- string底层原理
- 运算符
- new
- make
- 指针
- 下划线 & import
- 语法糖
- 简短变量申明
- 流程控制
- ifelse
- switch
- select
- select实现原理
- select常见案例
- for
- range
- range实现原理
- 常见案例
- range陷阱
- Goto&Break&Continue
- Go函数
- 函数
- 可变参数函数
- 高阶函数
- init函数和main函数
- 匿名函数
- 闭包
- 常用内置函数
- defer
- defer常见案例
- defer规则
- defer与函数返回值
- defer实现原理
- defer陷阱
- 数据结构
- slice
- slice内存布局
- slice&array
- slice底层实现
- slice陷阱
- map
- Map实现原理
- 集合
- List
- Set
- 线程安全数据结构
- sync.Map
- Concurrent Map
- 面向对象编程
- struct
- 匿名结构体&匿名字段
- 嵌套结构体
- 结构体的“继承”
- struct tag
- 行为方法
- 方法与函数
- type Method Value & Method Expressions
- interface
- 类型断言
- 多态
- 错误机制
- error
- 自定义错误
- panic&recover
- reflect
- reflect包
- 应用示例
- DeepEqual
- 反射-fillObjectField
- 反射-copyObject
- IO
- 读取文件
- 写文件
- bufio
- ioutil
- Go网络编程
- tcp
- tcp粘包
- udp
- HTTP
- http服务
- httprouter
- webSocket
- go并发编程
- Goroutine
- thread vs goroutine
- Goroutine任务取消
- 通过channel广播实现
- Context
- Goroutine调度机制
- goroutine调度器1.0
- GMP模型调度器
- 调度器窃取策略
- 调度器的生命周期
- 调度过程全解析
- channel
- 无缓冲的通道
- 缓冲信道
- 单向信道
- chan实现原理
- 共享内存并发机制
- mutex互斥锁
- mutex
- mutex原理
- mutex模式
- RWLock
- 使用信道处理竞态条件
- WaitGroup
- 工作池
- 并发任务
- once运行一次
- 仅需任意任务完成
- 所有任务完成
- 对象池
- 定时器Timer
- Timer
- Timer实现原理
- 周期性定时器Ticker
- Ticker对外接口
- ticker使用场景
- ticker实现原理
- ticker使用陷阱
- 包和依赖管理
- package
- 依赖管理
- 测试
- 单元测试
- 表格测试法
- Banchmark
- BDD
- 常用架构模式
- Pipe-filter pattern
- Micro Kernel
- JSON
- json-内置解析器
- easyjson
- 性能分析
- gc
- 工具类
- fmt
- Time
- builtin
- unsafe
- sync.pool
- atomic
- flag
- runtime
- strconv
- template