概览最简单版的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. WIN10 设置WEB

    Web服务器搭建步骤(Win10) 1.在“开始”菜单处打开“控制面板”. 2点击“程序”. 3.点击“启动或关闭Windows功能”. 4.对“Internet Information Servic ...

  2. node学习之express(1)

    1.前提是你安装了node,npm 2.此次我学习的网站是 汇智网 3.创建一个项目学习: npm init 按照提示,输入/不输入 项目的一些信息 安装express模块:npm install e ...

  3. java: -source 1.5 中不支持 diamond 运算符 ,lambadas表达式 2018-03-13 22:43:47 eleven十一 阅读数 876更多

  4. drf框架,restful接口规范,源码分析

    复习 """ 1.vue如果控制html 在html中设置挂载点.导入vue.js环境.创建Vue对象与挂载点绑定 2.vue是渐进式js框架 3.vue指令 {{ }} ...

  5. vue+vuex项目中怎么实现input模糊查询

    1,首先给input框添加方法,但是用的是element-ui的组件,对input进行了封装,不能直接用原生的方法!,在element组件中,input框中方法有实例参数$event,代表事件对象  ...

  6. python练习:编写一个程序,检查3个变量x,y,z,输出其中最大的奇数。如果其中没有奇数,就输出一个消息进行说明。

    python练习:编写一个程序,检查3个变量x,y,z,输出其中最大的奇数.如果其中没有奇数,就输出一个消息进行说明. 笔者是只使用条件语句实行的.(if-else) 重难点:先把三个数进行由小到大的 ...

  7. Python - 字符串为多行时,转换为列表

    例如一个IP代理池,这是个字符串,有多行 120.236.137.65:8060 193.112.208.216:8118 121.8.98.198:80 121.8.98.197:80 121.8. ...

  8. vscode解决java无法输入(scanner)问题

    vscode解决java无法输入问题 需要先安装java环境,->windows安装java 新建Test.java 输入代码 import java.util.Scanner; public ...

  9. vue中使用vue-i18n 一个简单的国际化操作

    1.安装:npm install vue-i18n --save-dev 2.在main.js文件中引入: import VueI18n from 'vue-i18n' Vue.use(VueI18n ...

  10. Go类型断言

    package main import ( "fmt" ) type Point struct { x int y int } func main() { var a interf ...