• 【协作式原创】查漏补缺之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中锁的那些姿势,估计你不知道

  • 相关阅读:
    企业——给Zabbix部署onealert云告警平台
    企业——使用API在Zabbix监控中实现查看、创建、删除监控主机的功能
    企业——监控平台Zabbix的安装
    企业——Redis的高可用的搭建
    企业——Redis的主从复制的搭建
    企业——MYSQL异步复制、半同步复制、全同步复制
    企业——memcache对PHP页面的缓存加速优化
    企业——HAproxy实现负载均衡及其常见配置
    企业——LVS 负载均衡的八种调度算法
    PHP微信公众号开发之自动回复
  • 原文地址:https://www.cnblogs.com/yudidi/p/12307839.html
Copyright © 2020-2023  润新知