go锁基础 - atomic、sema

兜兜零元  金牌会员 | 2024-1-21 03:02:08 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 887|帖子 887|积分 2661

atomic和sema是实现go中锁的基础,简单看下他们的实现原理。
atomic

`atomic 常用来作为保证原子性的操作。
当多个协程,同时一个数据进行操作时候,如果不加锁,最终的很难得到想要的结果。
  1.   var p int64 = 0
  2.   func add() {
  3.           p = p + 1
  4.   }
  5.   func main() {
  6.           for i := 0; i < 1000; i++ {
  7.                   go add()
  8.           }
  9.           time.Sleep(time.Second * 5)
  10.           fmt.Println(p) //982
  11.   }
复制代码
这种情况下,最终打印的 都不会是1000,每次不固定。
  1. 改成atomic 能解决
  2. var p int64 = 0
  3. func add() {
  4.         atomic.AddInt64(&p, 1)
  5. }
  6. func main() {
  7.         for i := 0; i < 1000; i++ {
  8.                 go add()
  9.         }
  10.         time.Sleep(time.Second * 5)
  11.         fmt.Println(p)
  12. }
复制代码
atomic 为什么能做到?
  1. TEXT        sync∕atomic·AddInt64(SB), NOSPLIT, $0-24
  2.         GO_ARGS
  3.         MOVD        $__tsan_go_atomic64_fetch_add(SB), R9
  4.         BL        racecallatomic<>(SB)
  5.         MOVD        add+8(FP), R0        // convert fetch_add to add_fetch
  6.         MOVD        ret+16(FP), R1
  7.         ADD        R0, R1, R0
  8.         MOVD        R0, ret+16(FP)
  9.         RET
复制代码
老的版本中是能见到,lock 这种操作系统级别的锁,新版的go已经改写了这块逻辑,但是能猜想到效果肯定一样。 如果有理清楚的,评论区可以交流下。
小结:
  1. 原子操作是一种硬件层面加锁的机制
  2. 保证操作一个变量的时候,其他协程/线程无法访问
复制代码
sema

几乎在go的每个锁的定义都能看到sema的身影,理解了sema再看 互斥锁、读写锁就会很好理解。
  1. 信号量锁/信号锁
  2. 核心是一个uint32值,含义是同时可并发的数量
  3. 每一个sema 锁都对应一个SemaRoot结构体
  4. SemaRoot中有一个平衡二叉树用于协程排队
复制代码
例如:
  1. type Mutex struct {
  2.           state int32
  3.           sema  uint32
  4. }
  5. sema的uint32中的 每一个数,背后都对应一个  semaRoot的结构体
  6. type semaRoot struct {
  7.         lock  mutex
  8.         treap *sudog        // root of balanced tree of unique waiters.
  9.         nwait atomic.Uint32 // Number of waiters. Read w/o the lock.
  10. }
  11. type sudog struct {
  12.         g *g              // 包含了 协程 g
  13.         next *sudog   // 下一个
  14.         prev *sudog  
  15.         elem unsafe.Pointer // data element (may point to stack)
  16. }
复制代码
结构如下:
这里可以先讲下,当这个 sema  uint32 值,初始化时候,大于0 比如 赋值5,就代表着,并发时候,有5个协程可以获取锁。其他协程需要等待前面5个释放了,才能进入。
sema 大于0
  1.    // 获取sema锁。大于0的情况
  2. func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int, reason waitReason) {
  3.             gp := getg()
  4.             if gp != gp.m.curg {
  5.                     throw("semacquire not on the G stack")
  6.             }
  7.             // Easy case. // 容易的情况
  8.             if cansemacquire(addr) {
  9.                     return
  10.             }
  11.       // 方法很长,先看简单的部分。
  12. }
  13.   
  14. func cansemacquire(addr *uint32) bool {
  15.         for {
  16.                 v := atomic.Load(addr) // 根据sema的地址,获取int32的值
  17.                 if v == 0 {   // 如果未0了,就获取失败了
  18.                         return false
  19.                 }
  20.         // 大于0 ,则把 sema的值减去1
  21.                 if atomic.Cas(addr, v, v-1) { // cas 就是 CompareAndSwapInt 的底层实现
  22.                         return true
  23.                 }
  24.         }
  25. }
复制代码
到此,对sema为什么只是定义为一个 uint32的值有了大致理解,就是一个控制能有多少个协程同时获取锁的值。
看下释放:
  1. func semrelease1(addr *uint32, handoff bool, skipframes int) {
  2.         root := semtable.rootFor(addr)
  3.         atomic.Xadd(addr, 1) // 给sema的值 加上1
  4.         // Easy case: no waiters?
  5.         // This check must happen after the xadd, to avoid a missed wakeup
  6.         // (see loop in semacquire).
  7.         if root.nwait.Load() == 0 { // 如果没有 nwait在等待,就直接结束。
  8.                 return
  9.         }
  10. }
复制代码
nwait 就是等待协程的个数。
小结, 当sema的值大于0 :
  1. 获取锁:uint32减1 ,获取成功
  2. 释放锁:uint32加1,释放成功
复制代码
sema值等于0

再看 semacquire1
  1. func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int, reason waitReason) {
  2.      / / Harder case:
  3.         //        increment waiter count
  4.         s := acquireSudog()
  5.         root := semtable.rootFor(addr) // 根据sema的地址,获取了包含 sudog的队列
  6.         for {
  7.                 root.queue(addr, s, lifo) // 将新的协程放入这个等待队列中
  8.                 goparkunlock(&root.lock, reason, traceBlockSync, 4+skipframes)
  9.         // 主动调用协程的gopark,让它休眠,gopark的说明看 go GMP中有讲
  10.         }
  11.         releaseSudog(s)
  12. }
  13. //再看释放
  14. func semrelease1(addr *uint32, handoff bool, skipframes int) {
  15.         root := semtable.rootFor(addr)
  16.         atomic.Xadd(addr, 1)
  17.         if root.nwait.Load() == 0 {
  18.                 return
  19.         }
  20.     // 如果等待队列不是0 ,就需要释放一个
  21.         // Harder case: search for a waiter and wake it.
  22.         lockWithRank(&root.lock, lockRankRoot)
  23.        
  24.         s, t0 := root.dequeue(addr) // 从全局的队列中,取出一个
  25.         if s != nil {
  26.                 root.nwait.Add(-1) // 把等待的数量减一
  27.         }
  28.         unlock(&root.lock) //操作全局队列都需要加锁
  29. }
复制代码
小结,当sema的值等于0时候:
  1. 获取锁:协程休眠,进入堆树等待
  2. 释放锁:从堆树中取出一个协程,唤醒
  3. sema 锁退化成一个专用休眠队列
复制代码
有没有可能sema的值,小于0 ?
  1. 看看sema的定义 `uint32` 所以 不可能。
复制代码
总结下:
  1. atomic原子操作是一种硬件层面的加锁机制。
  2. sema 背后是一整套锁的管理和等待的机制,开发者在使用时候,感知不到。
  3. sema的值就是能同时获取锁协程的个数。sema的地址作为了休眠等待队列(平衡树)的key。
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

兜兜零元

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表