Go 语言中的 sync.Mutex 学习
极客时间专栏,鸟窝大佬的 Go 并发编程课程,由于大佬的课程十分精彩,领悟需要时间与精力!所以再次本文仅仅只能作为知识总结,相应具体内容与源码分析请去极客时间上购买大佬相应的课程!
Mutex
Go 语言中的 sync 包的 mutex 的设计,有四个演变阶段。
- 1、初版的 Mutex 采用一个 flag 表示锁是否被持有,实现比较简单
- 2、之后为了照顾新来的 Goroutine(下文简称 G ),会让新人能够尽可能的优先获取锁,此为第二个阶段
- 3、第三个阶段呢,是使被唤醒的 G 与新来的 G 有更多的机会竞争锁,但是这样会引发相应的饥饿问题,所以目前又加入了饥饿的解决方案
- 4、第四个即为解决饥饿的阶段。
如下图所示
初版 mutex 的实现
1 |
|
· 其中,CAS 为一种指令,即将给定的值与内存地址中的值进行相比较,如果是同一个值,就用新值替换内存地址中的旧值。而且 CAS 操作指令是原子性的指令(即数据库中原子性的概念,修改不了数据,事务回滚到修改之前的数据,数据不改变)
有趣的事情是,Unlock 方法能被任意的 G 调用释放,即使没有持有互斥锁的 G,也能进行相应的操作。
所以在使用 Mutex 的时候,必须保证 G 尽可能不去释放自己未持有的锁,一定遵循 “谁申请,谁释放” 的原则。一般在使用 Mutex 的时候,Lock 与 Unlock 方法都应该在一个方法内成对出现。
在1.14版本中 Go 对 defer 做了相应的优化,采取更有效的内联模式,将之前生成的 defer 对象放入 defer chain 中,所以 defer 对程序执行的影响微乎其微了。
缺点:请求锁时候,G 会排队等待获取互斥锁,虽然看起来挺公平的,但是从性能上来看,并非最优的解法。如果能将锁让给正在用 CPU 时间片的 G 的话,就不需要做上下文的切换,在高并发的情况下,可能会有更好的性能。
“给新人机会” 阶段
2011 年 6 月 30 日,Go 语言开发者在 commit 中对 Mutex 做了一次大调整,调整后的 Mutex 实现如下:
1 |
|
其中 Mutex 此版本的设计思想为将第一个 int32 类型的 state 字段,划算为二进制,按二进制的位数进行区分,如下图:
这样可能以最小的内存来实现互斥锁结构,最低位表示锁是否被占有(1|0 占有|非占有),次低位表示锁是否有被唤醒的 G,其余 30 位表示等待此锁的 G 的数量。与计算机网络的子网划分很相似的设计,一个数值,分为三部分,代表三个意义。
并且因为 atomic 原子性包的添加,请求锁Lock也变复杂了,。
1 |
|
使用 mutex 的一些注意事项
1、能不用 mutex, 尽量不用 mutex, 使用读写锁合适
2、尽量使用 defer 释放锁,防止因为 panic 而导致锁未释放
3、mutex.Lock() 后是不可重入的,写递归时候,不能调用 mutex
4、尽量使用读写锁!sync.RLock/RUnlock, sync.WLock/WUnlock