zoukankan      html  css  js  c++  java
  • 【协作式原创】查漏补缺之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

    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。

  • 相关阅读:
    Runtime类与Process类
    runtime 得到jvm的内存空间信息
    random 类的实际应用
    NumberFormat类实际应用
    java 国际化程序实现
    Calendar类取得系统的当前日期
    demo-文件复制
    用递归方法 在列表中 使用二分查找。找一个数字
    第一个 购物车程序
    练习题1 —— 全局替换程序
  • 原文地址:https://www.cnblogs.com/yudidi/p/12266904.html
Copyright © 2011-2022 走看看