概览最简单版的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

  1. 无冲突 通过CAS操作把当前状态设置为加锁状态。
  2. 有冲突 开始自旋(CAS函数返回false),并等待锁释放,如果其他携程在这段时间内释放了该锁,直接获得该锁;如果没有释放,进入3
  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加锁.

  1. 第一次有3个请求,形成3个携程,g1成功获取锁,g2,g3堵塞排队。
  2. 然后又来3个请求,形成3个携程,g4,g5,g6,且还未阻塞。
  3. g1释放锁, g2被唤醒,和g456同时争抢锁,g456抢到锁的概率是更大的。(新请求锁的携程存在一个优势,它们已经在CPU上运行且它们数量很多) // 参考附录10

为了解决公平性问题,1.9版本引入了饥饿模式。

1.12版本 暂时略

饥饿模式与正常模式

参考附录10和附录6

go1.9版本之后是饥饿模式

正常模式: 解锁后,被唤醒的携程和新来的携程会一起竞争锁,由于新来的携程在CPU上运行且数量很多,所以一般是新来的携程竞争成功,所以唤醒队列里面的携程容易饥饿。

饥饿模式: 新来的携程不参与竞争。

饥饿模式切回正常模式: 略。

mutex源码总结和借鉴

  1. mutex底层是通过CAS实现的,演进了3个版本,第二版增加了有限自旋,减少了携程的阻塞和唤醒, 第三版增加了饥饿模式,从不公平的锁变为公平锁。
  2. 锁模式和公平性
  3. iota和4个位操作(设置,清空,查询,左移右移):给标志位设置1(|或操作),清空标志位(&^按位清除(a与上非b)操作),获取标志位的值(&与操作)

剩余部分,看时间决定是否讲

Q: go有哪些锁

A:

  1. 所有锁的都可以归为4大类: 1. 共享锁,独占锁 2. 悲观锁,乐观锁
  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个字段即可:

  1. owner:持有该自旋锁的携程Id.
  2. 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源码实现的更多相关文章

  1. 【协作式原创】查漏补缺之Golang中mutex源码实现(预备知识)

    预备知识 CAS机制 1. 是什么 参考附录3 CAS 是项乐观锁技术,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是 ...

  2. 半夜思考之查漏补缺, 在 Spring中, 所有的 bean 都是 Spring 创建的吗 ?

    Spring 是一个 bean 容器, 负责 bean 的创建, 那么所有的 bean对象都是 Spring 容器创建的吗 ? 答案是否定的. 但是乍一想, 好像所有的对象都是 Spring 容器负责 ...

  3. 查漏补缺:Vector中去重

    对于STL去重,可以使用<algorithm>中提供的unique()函数. unique()函数用于去除相邻元素中的重复元素(所以去重前需要对vector进行排序),只留下一个.返回去重 ...

  4. Java查漏补缺(3)(面向对象相关)

    Java查漏补缺(3) 继承·抽象类·接口·静态·权限 相关 this与super关键字 this的作用: 调用成员变量(可以用来区分局部变量和成员变量) 调用本类其他成员方法 调用构造方法(需要在方 ...

  5. Java基础查漏补缺(2)

    Java基础查漏补缺(2) apache和spring都提供了BeanUtils的深度拷贝工具包 +=具有隐形的强制转换 object类的equals()方法容易抛出空指针异常 String a=nu ...

  6. CSS基础面试题,快来查漏补缺

    本文大部分问题来源:50道CSS基础面试题(附答案),外加一些面经. 我对问题进行了分类整理,并给了自己的回答.大部分知识点都有专题链接(来源于本博客相关文章),用于自己前端CSS部分的查漏补缺.虽作 ...

  7. Go语言知识查漏补缺|基本数据类型

    前言 学习Go半年之后,我决定重新开始阅读<The Go Programing Language>,对书中涉及重点进行全面讲解,这是Go语言知识查漏补缺系列的文章第二篇,前一篇文章则对应书 ...

  8. 《CSS权威指南》基础复习+查漏补缺

    前几天被朋友问到几个CSS问题,讲道理么,接触CSS是从大一开始的,也算有3年半了,总是觉得自己对css算是熟悉的了.然而还是被几个问题弄的"一脸懵逼"... 然后又是刚入职新公司 ...

  9. js基础查漏补缺(更新)

    js基础查漏补缺: 1. NaN != NaN: 复制数组可以用slice: 数组的sort.reverse等方法都会改变自身: Map是一组键值对的结构,Set是key的集合: Array.Map. ...

随机推荐

  1. c语言用raw socket进行抓包

    https://www.cnblogs.com/MrYuan/p/5215923.html https://blog.csdn.net/qq_41787205/article/details/8669 ...

  2. Multisim 如何添加文本 如何编辑文本字体

    1.在Multisim中如何添加文本 方法1)Place -> Text 方法2)Ctrl+T 2.如何修改字体的大小及颜色

  3. go使用错误概览

    1. 解决:GO语言中要提供给外面访问的方法或是结构体必须是首字母大写.这个结构体只有结构体名大写了,而里面的字段没有首字母大写,而GO语言在模板调用时应该认为是两个不同的过程,所以找不到值.于是把结 ...

  4. if (flag)

    var flag = false;//暂停标记if (flag){ alert("aa")}else { alert("bb")}bb if (flag) = ...

  5. JS中constructor属性

    constructor属性用于对当前对象的构造函数的引用.可以用来判断对象的类型: <script> var newStr = new String("One world One ...

  6. composer update 或者 composer install提示killed解决办法

    出现此原因大多因为缓存不足造成,在linux环境可增加缓存解决. free -mmkdir -p /var/_swap_cd /var/_swap_#Here, 1M * 2000 ~= 2GB of ...

  7. python 中对list去重

    本文去重的前提是要保证顺序不变,本文给出了多种实现方法,需要的朋友可以参考下 1.直观方法 最简单的思路就是: ids = [1,2,3,3,4,2,3,4,5,6,1] news_ids = [] ...

  8. k8s搭建

    K8s官方文档地址:https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/  如果用云主机部 ...

  9. 解决 上下拉的橡皮筋 和 复制无效 和 ios滑动屏幕定时器停止问题cordova

    cordova 安装插件cordova plugin add cordova-plugin-wkwebview-engine@latest --save config.xml 的 <platfo ...

  10. java用JSONObject生成json

    Json在前后台传输中,是使用最多的一种数据类型.json生成的方法有很多,自己只是很皮毛的知道点,用的时候,难免会蒙.现在整理下 第一种: import net.sf.json.JSONArray; ...