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


    预备知识

    CAS机制

    1. 是什么

    参考附录3

    CAS 是项乐观锁技术,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
    CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。
    如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)
    CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查 + 数据更新的原理是一样的。

    扩展:
    Q: 悲观锁和乐观锁的主要区别.
    A:
    乐观锁每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。
    悲观锁在读取数据也会加锁,甚至只能有1个线程可以读取数据。

    2. 图解CAS

    讲解参考附录1的图解.

    参考i++如何在并发情况下产生问题的

    package main
    
    var i int32 = 1
    
    func main() {
        i++
    }
    
    • 反汇编查看第6行的汇编代码,可以发现和上图中C代码类似,也是对应3个底层汇编指令。(注意单个汇编指令不一定是原子操作,只是我们分析只考虑汇编指令之间进行中断和切换)
    $ go tool compile -S main.go |grep main.go:6
            0x0012 00018 (main.go:6)        PCDATA  $2, $0
            0x0012 00018 (main.go:6)        PCDATA  $0, $0
            0x0012 00018 (main.go:6)        MOVL    "".i(SB), AX // 根据i的内存地址(addr1),加载i值到寄存器
            0x0018 00024 (main.go:6)        INCL    AX           // 寄存器执行自增1操作
            0x0019 00025 (main.go:6)        MOVL    AX, "".i(SB) // 寄存器的值更新给i对应的内存地址(addr1)
    
    • 关键问题:
      Q: 什么叫冲突
      A: 内存实际值和携程预期值不等就是冲突。 就是携程发现内存实际值(11)!=old值(10)就是冲突,本来该携程是要对10进行加1操作的.

    3. Go中的CAS操作代码示例

    讲解参考附录2的代码.

    // CompareAndSwapInt64 executes the compare-and-swap operation for an int64 value.
    func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
    

    预备知识点: atomic.LoadInt64和atomic.StoreInt64,是一对原子操作,分别是读取和写入一个int。
    TODO: 目前不清楚附录2中提到的错误写法有啥问题。

    • 扩展
      Q: 这里引出一个新的问题,既然CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?或者问CompareAndSwapInt64函数为啥能保证原子性呢? 或者问atomic包中的函数怎么保证的原子性?
      A: CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。至于硬件层面是怎么保证的参考该博客的后半部分

    4. cas机制的缺点

    1. 高并发情况下自旋消耗cpu.(也就是compare老是失败)
    2. 只支持一个变量的原子操作,不支持多个变量(代码块)的原子操作. 代码块的原子操作,用sync.Mutex。
    3. CAS有一种特殊情况:ABA。ABA是否会导致问题需要具体来分析: (参考附录4)
      3.1 没有问题的case: 对i加一减一,导致Compare成功,其实是不会影响运行结果的。
      3.2 有问题的case: Real-world examples for ABA in multithreading

    位操作和Go特殊语法iota

    • &(与操作)
      1&1=1,1&0=1,0&0=1
    func main() {
    	var state int32
    	mutexLocked := int32(1)
    	mutexWoken := int32(2)
    	// & 与运算,只有两者都是1结果才是1.
    	fmt.Println(state&mutexLocked, state&mutexWoken)
    	state = mutexLocked
    	fmt.Println(state&mutexLocked, state&mutexWoken) // ...001 & ...001 = ...001
    	state = mutexWoken
    	fmt.Println(state&mutexLocked, state&mutexWoken) // ...010 & ...010 = ...010
    }
    //output
    0 0
    1 0
    0 2
    
    • 左移操作 a<<n: a左移n位
    1<<0 = 1 = 1
    1<<1 = 10 = 2
    1<<2 = 100 = 4
    
    • Golang特殊语法:iota
      参考附录5
    const (
    	mutexLocked      = 1 << iota // mutex is locked // 1左移iota(0)位赋值给变量 // iota = 0
    	mutexWoken                   // 1左移iota(1)位赋值给变量 // iota = 1
    	mutexStarving                // 1左移iota(2)位赋值给变量 // iota = 2
    	mutexWaiterShift = iota      // iota = 3
    )
    
    func main() {
    	fmt.Println(mutexLocked, mutexWoken, mutexStarving, mutexWaiterShift)
    }
    //output
    1 2 4 3
    
    • 按位清除(&^)
    1. 执行规则: a &^ b == a & (^b),即与上非b。
    2. 语句用途: 把a中某些位置清空(置0),某些位置是指b中为1的位置。
      一般是 待清空的数a &^ 标志位为1的等长数字b == 执行后,标志位被清空,其他数字不变。
    package main
    import (
    	"fmt"
    )
    func main() {
    	// new &^= mutexWoken,就是清除mutexWoken位,置为 0
    	fmt.Println(1 &^ 1)
    	fmt.Println(0 &^ 1)
    	fmt.Printf("%b
    ",1 & ^1)
    	fmt.Printf("%b
    ",0 & ^1)
    }
    //output : 从执行结果看,
    0
    0
    0
    0
    
    • 设置,清空和读取1个标志位
    package main
    import (
    	"fmt"
    )
    // 1.3 与 1.7 老的实现共用的常量
    const (
        mutexLocked = 1 << iota // mutex is locked
        mutexWoken
    )
    var state int32
    func main() {
    	fmt.Printf("%b,%b,%b
    ",state,mutexLocked,mutexWoken)
    	// 1. "|"写入标志位(置1)
    	wokenState := state | mutexWoken
    	wokenState |= mutexLocked
    	fmt.Printf("%b,%08b
    ",wokenState,wokenState) // %08b: 输出8位,位数不足填充0
    	fmt.Printf("%b,%b
    ",wokenState & mutexLocked,wokenState & mutexWoken) // 3.读取标志位. 如果标志位是1,读取结果就是mutexLocked或mutexWoken
    	// 2. "&^"清空标志位,即置0
    	wokenState &^= mutexWoken
    	fmt.Printf("%08b
    ",wokenState) 
    }
    //output
    0,1,10
    11,00000011
    1,10
    00000001
    

    raceenabled是什么代码

    参考附录11.
    raceenabled相关的代码全部忽略,这是golang内部使用thread-sanitizer用于扫描线程安全问题的诊断代码。

    参考资料

    1.图解CAS
    2.GO中CAS代码示例
    3.CAS乐观锁,最精确的文字描述.
    4.CAS会导致"ABA问题"
    5.Go-iota使用例子
    6.sync.Mutex的实现和演进
    7.Go语言中你所不知道的位操作用法 TODO: 通过位操作实现用户类型的存储,更新和修改。如[一个qq号可以用VIP会员,SVIP超级会员,蓝钻用户,黄钻用户,红钻用户....]
    8.位清除(&^)有哪些应用?
    9.go语言中获取变量类型的三种方法
    10.Golang中的int类型和uint类型到底有多大? TODO: int和uint是根据 CPU 变化的!!!
    11.毛剑-1.3sync.Mutex源码解析
    Go中锁的那些姿势,估计你不知道

  • 相关阅读:
    HDOJ 4747 Mex
    HDU 1203 I NEED A OFFER!
    HDU 2616 Kill the monster
    HDU 3496 Watch The Movie
    Codeforces 347A A. Difference Row
    Codeforces 347B B. Fixed Points
    Codeforces 372B B. Hungry Sequence
    HDU 1476 Sudoku Killer
    HDU 1987 How many ways
    HDU 2564 词组缩写
  • 原文地址:https://www.cnblogs.com/yudidi/p/12307839.html
Copyright © 2020-2023  润新知