【协作式原创】查漏补缺之Golang中mutex源码实现
概览最简单版的mutex(go1.3版本)
预备知识
主要结构体
type Mutex struct {
state int32 // 指代mutex锁当前的状态
sema uint32 // 信号量,用于休眠或唤醒goroutine
}
31 2 1 0
+----~~~------+-+-+
// 1.3 与 1.7 老的实现共用的常量
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexWaiterShift = iota
)
// 1.12 公平锁使用的常量
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
starvationThresholdNs = 1e6
)
- state字段: 4字节int, 其中低几位用于做标记,高位地址空间用于计数,表示有多少个 goroutine 正在等待而处于休眠中。分成四部分来看:
1. [31,3]前29位: 表示有多少个 goroutine 正在等待而处于休眠中
2. [2]: 是否饥饿模式 0表示正常模式,1表示饥饿模式
3. [1]: 是否被唤醒. 0表示没有唤醒,1表示mutex已被唤醒,可以唤醒等待其它goroutine.
4. [0]: 是否加锁. 1表示加锁
mutexLocked 值为1,根据 mutex.state & mutexLocked 得到 mutex 的加锁状态,结果为1表示已加锁,0表示未加锁
mutexWoken 值为2(二进制:10),根据 mutex.state & mutexWoken 得到 mutex 的唤醒状态,结果为1表示已唤醒,0表示未唤醒
mutexStarving 值为4(二进制:100),根据 mutex.state & mutexStarving 得到 mutex 的饥饿状态,结果为1表示处于饥饿状态,0表示处于正常状态
mutexWaiterShift 值为3,根据 mutex.state >> mutexWaiterShift 得到当前等待的 goroutine 数目
- sema字段: 信号量
Lock源码-分1.3,1.7,1.12版本来讲
go1.3源码
参考附录6
- lock()
Lock方法申请对mutex加锁,Lock执行的时候,分三种情况 // 参考附录10
- 无冲突 通过CAS操作把当前状态设置为加锁状态。
- 有冲突 开始自旋(CAS函数返回false),并等待锁释放,如果其他携程在这段时间内释放了该锁,直接获得该锁;如果没有释放,进入3
- 有冲突,且已经过了自旋阶段(CAS函数返回true),执行runtime_Semacquire函数使当前携程休眠。
func (m *Mutex) Lock() {
// 情况1: CAS1直接加锁成功
// Fast path: grab unlocked mutex.
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
awoke := false
for {
// 构造新的state(new),并设置Locked和Woken位,随后试图通过CAS2把m.state设置为new.
old := m.state
new := old | mutexLocked
if old&mutexLocked != 0 {
new = old + 1<<mutexWaiterShift
}
if awoke {
// The goroutine has been woken from sleep,
// so we need to reset the flag in either case.
new &^= mutexWoken
}
// 情况2: CAS2返回false。这种一般是高并发,多个携程同时修改锁状态,导致进入自旋。
if atomic.CompareAndSwapInt32(&m.state, old, new) {// 情况3: CAS2返回true,执行下列代码
// 3.1 old本来是未加锁的(Locked从0更新为1),那么CAS2把Locked位从0置为1,就是抢锁成功,直接break返回。
if old&mutexLocked == 0 {
break
}
// 3.2 old本来就是加锁的(Locked从1更新为1),那么说明CAS2只是把waiter+1,并没有修改Locked的值,进入休眠。
runtime_Semacquire(&m.sema)
awoke = true
}
}
}
简化版:
// 情况1: CAS1直接加锁成功
if CAS1()
// 构造新的state(new),并设置Locked和Woken位,随后试图通过CAS2把m.state设置为new.
其他代码...
// 情况2: CAS2返回false。这种一般是高并发,多个携程同时修改锁状态,导致进入自旋。
for CAS2(){ // 情况3: CAS2返回true,执行下列代码
// 3.1 old本来是未加锁的,那么CAS2把Locked位从0置为1,就是抢锁成功,直接break返回。
// 3.2 old本来就是加锁的(Locked为1),那么说明CAS2只是把waiter+1,并没有修改Locked的值,进入休眠。
- unlock()
1. Locked置0
2. 构造新state(waiter-1;woken置1,表唤醒)
3. if CAS(){ // false:有冲突,Compare失败
// true:更新成功,唤醒排队的携程
}
Q: 什么情况会走情况2呢?
A: 高并发的情况下. 10个携程同时读取old,然后修改,只有第一个携程首先执行CAS1/CAS2成功更新state,其他9个执行CAS2时就会Compare失败,继续循环,依次类推,分别有[9,8,...,1]次Compare失败的情况。(这个是基于我的理解做的推测)Q: 什么情况会走情况3呢?
A: 下面用3个携程加锁解锁过程,分析state的变化.
没有人加锁,state(0000)
1. g1,抢锁成功 0000->0001
2. g2,抢锁 0001->1001, runtime_Semacquire进入阻塞
3. g3,抢锁 1001->2001, runtime_Semacquire进入阻塞
4. g1,释放锁2001->1010 [Unlock(Locked置0, waiter-1, woken置1, runtime_Semrelease唤醒休眠携程]
5. g2和g3都被唤醒,执行Lock的(awoke = true)
6. g2先执行,读取old值(1010,注意Locked是0),执行CAS2成功,则1010->1001[执行Lock的(Locked置1,waiter不变,woken置0,执行CAS2成功,break加锁成功)]。// g2就是情况3.1(Locked从0更新为1)
7. 此时g2还未释放锁。
8. g3执行,读取old值(1001,注意Locked是1),执行CAS2成功,则1001->2001[执行Lock的(Locked置1,waiter+1,woken置0,执行CAS2成功,然后runtime_Semacquire阻塞)] // g3就是情况3.2(Locked从1更新为1,并未加锁成功,所以只是waiter+1)
9. g2释放锁 2001->1010 [Unlock(Locked置0, waiter-1, woken置1, runtime_Semrelease唤醒休眠携程]
10.g3释放锁 1010->0010 [Unlock(Locked置0,判断old值的mutexLocked|mutexWoken任一为1直接return,解锁完成]
所有携程执行完毕,state变成(0010)
Q: 1.3版本的缺点
参考附录6-前言。
A: 1.3版本的CAS实现的朴素互斥锁,存在一定runtime调度浪费,即,有些携程休眠之后马上就唤醒了,不如先自旋一会儿,如果仍然不能获取锁,再休眠也不迟。
Q: 为什么1.7要引入有限自旋逻辑?
A: 减少情况3中携程的休眠情况,也就减少了休眠后又唤醒的资源开销,增加有限的资源操作,自旋仍没有获取锁,才会休眠。
增加了自旋 spinlock 的逻辑,因为大部份被mutex加锁的代码执行时间很短,自旋可以减小无谓的runtime调度。推荐看官方spin commit
Q: 什么叫自旋
A: busy-waiting,忙等状态。
1.7版本
参考附录6
1.7版本对比1.3版本
增加了自旋操作
Q: runtime_canSpin和runtime_doSpin的作用
A: 参考附录11
runtime_canSpin:通过4个条件判断是否可以自旋。
runtime_doSpin: 占着茅坑(CPU)不拉屎。
1.7版本缺点
- 不公平 (新请求锁的携程比被唤醒的携程更容易获得锁)
我个人是这么理解: 比如高并发情况下,对商品数量n加锁.
- 第一次有3个请求,形成3个携程,g1成功获取锁,g2,g3堵塞排队。
- 然后又来3个请求,形成3个携程,g4,g5,g6,且还未阻塞。
- g1释放锁, g2被唤醒,和g456同时争抢锁,g456抢到锁的概率是更大的。(新请求锁的携程存在一个优势,它们已经在CPU上运行且它们数量很多) // 参考附录10
为了解决公平性问题,1.9版本引入了饥饿模式。
1.12版本 暂时略
饥饿模式与正常模式
参考附录10和附录6
正常模式: 解锁后,被唤醒的携程和新来的携程会一起竞争锁,由于新来的携程在CPU上运行且数量很多,所以一般是新来的携程竞争成功,所以唤醒队列里面的携程容易饥饿。
饥饿模式: 新来的携程不参与竞争。
饥饿模式切回正常模式: 略。
mutex源码总结和借鉴
- mutex底层是通过CAS实现的,演进了3个版本,第二版增加了有限自旋,减少了携程的阻塞和唤醒, 第三版增加了饥饿模式,从不公平的锁变为公平锁。
- 锁模式和公平性
- iota和4个位操作(设置,清空,查询,左移右移):给标志位设置1(|或操作),清空标志位(&^按位清除(a与上非b)操作),获取标志位的值(&与操作)
剩余部分,看时间决定是否讲
Q: go有哪些锁
A:
- 所有锁的都可以归为4大类: 1. 共享锁,独占锁 2. 悲观锁,乐观锁
- 按照这个分类方法,sync.Mutex(独占锁,互斥锁,悲观锁),sync.RWMutex(共享锁)
Q: 什么是自旋锁
Q: go实现自旋锁
type spinLock uint32
func (sl *spinLock) Lock() {
for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
runtime.Gosched()
}
}
func (sl *spinLock) Unlock() {
atomic.StoreUint32((*uint32)(sl), 0)
}
func NewSpinLock() sync.Locker {
var lock spinLock
return &lock
}
Q: 实现可重入的自旋锁
思路: 要使得一个锁可以多次Lock和unlock,给自旋锁增加2个字段即可:
- owner:持有该自旋锁的携程Id.
- count:持有锁的携程的加锁次数. 除了第一次加锁是调用cas加锁,剩下的加锁都是通过对count+1进行伪加锁.
应用场景: 附录9
参考资料
1.图解CAS
2.GO中CAS代码示例
3.CAS乐观锁,最精确的文字描述.
4.CAS会导致"ABA问题"
5.Go-iota使用例子
6.sync.Mutex的实现和演进
7.Go语言中你所不知道的位操作用法 TODO: 通过位操作实现用户类型的存储,更新和修改。如[一个qq号可以用VIP会员,SVIP超级会员,蓝钻用户,黄钻用户,红钻用户....]
8.golang 自旋锁
9.可重入锁的使用场景 TODO:
10.go夜读-sync.Mutex 源码分析
11.runtime_canSpin和runtime_doSpin
Go中锁的那些姿势,估计你不知道
TODO
乐观锁中,我们有3种常用的做法来实现:
https://www.cnblogs.com/cwfsoft/p/7759944.html
cas实现lockfree才会有ABA问题。ABA不是cas自己的问题。这是两回事。
cas理论上还分weak和strong。
【协作式原创】查漏补缺之Golang中mutex源码实现的更多相关文章
- 【协作式原创】查漏补缺之Golang中mutex源码实现(预备知识)
预备知识 CAS机制 1. 是什么 参考附录3 CAS 是项乐观锁技术,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是 ...
- 半夜思考之查漏补缺, 在 Spring中, 所有的 bean 都是 Spring 创建的吗 ?
Spring 是一个 bean 容器, 负责 bean 的创建, 那么所有的 bean对象都是 Spring 容器创建的吗 ? 答案是否定的. 但是乍一想, 好像所有的对象都是 Spring 容器负责 ...
- 查漏补缺:Vector中去重
对于STL去重,可以使用<algorithm>中提供的unique()函数. unique()函数用于去除相邻元素中的重复元素(所以去重前需要对vector进行排序),只留下一个.返回去重 ...
- Java查漏补缺(3)(面向对象相关)
Java查漏补缺(3) 继承·抽象类·接口·静态·权限 相关 this与super关键字 this的作用: 调用成员变量(可以用来区分局部变量和成员变量) 调用本类其他成员方法 调用构造方法(需要在方 ...
- Java基础查漏补缺(2)
Java基础查漏补缺(2) apache和spring都提供了BeanUtils的深度拷贝工具包 +=具有隐形的强制转换 object类的equals()方法容易抛出空指针异常 String a=nu ...
- CSS基础面试题,快来查漏补缺
本文大部分问题来源:50道CSS基础面试题(附答案),外加一些面经. 我对问题进行了分类整理,并给了自己的回答.大部分知识点都有专题链接(来源于本博客相关文章),用于自己前端CSS部分的查漏补缺.虽作 ...
- Go语言知识查漏补缺|基本数据类型
前言 学习Go半年之后,我决定重新开始阅读<The Go Programing Language>,对书中涉及重点进行全面讲解,这是Go语言知识查漏补缺系列的文章第二篇,前一篇文章则对应书 ...
- 《CSS权威指南》基础复习+查漏补缺
前几天被朋友问到几个CSS问题,讲道理么,接触CSS是从大一开始的,也算有3年半了,总是觉得自己对css算是熟悉的了.然而还是被几个问题弄的"一脸懵逼"... 然后又是刚入职新公司 ...
- js基础查漏补缺(更新)
js基础查漏补缺: 1. NaN != NaN: 复制数组可以用slice: 数组的sort.reverse等方法都会改变自身: Map是一组键值对的结构,Set是key的集合: Array.Map. ...
随机推荐
- 把jar包部署为linux服务
一直未配置成功,直到放弃后reboot了下,才直到错的不是自己的配置,而是自己不懂 1.在touch /etc/rc.d/init.d/tl_c_cons_service(创建新文件) 2.vi /e ...
- 高内存 高CPU 劣质网络下的测试
内存 先把系统的虚拟内存去掉 (右键我的电脑属性里有的.选择那个无分页文件 虚拟内存在任务管理器就不显示了), 然后机子本身内存不高,开几个网页就满了 CPU cpu可以用鲁大师测试cpu ...
- 优酷1080p的kux格式文件怎么转换为MP4格式?
直接使用优酷自己的FFMPEG解码! 格式为:"优酷ffmpeg.exe的安装地址" -y -i ".kux文件储存地址" -c:v copy -c:a cop ...
- Reinforcement Learning Algorithm 资源
算法源码: https://github.com/ljpzzz/machinelearning https://github.com/imraviagrawal/Reinforcement-Learn ...
- Spring - Spring Boot - 应用构建与运行
概述 spring boot 应用构建 spring boot 应用运行 背景 之前的看了看 Spring 的书, 结果老懒没实践 而且后续有别的想法, 但这个始终是第一步 1. 准备 知识 java ...
- Docker - 命令 - docker image
概述 docker 客户端操控 镜像 1. 分类 概述 1 简单对 命令 做一些分类 分类 查看 ls inspect history 与 dockerhub 交互 pull push 导出 & ...
- STA之RC网
STA的主要工作是计算电路网络的延时,如今的电路网络还是由CMOS cell和net组成的,所以STA所要计算的延时仍是电容的充放电时间.等量子计算机普及的时候,如今的这一套理论都将随着科技的进步被丢 ...
- 台式机windows10 进入安全模式
按住shift键不松,在登录界面点击重启,即可进入安全模式!!!!
- java爬虫出现java.lang.IllegalArgumentException: Illegal character in path at index 31
url地址中出现了空格,使用trim()函数去除空格就好了
- Nginx做代理
0 查看日志 tail -f /var/log/nginx/access.log 1 Nginx代理配置语法 1.Nginx代理配置语法 Syntax: proxy_pass URL; Default ...