sync.Cond

前言

本次的代码是基于go version go1.13.15 darwin/amd64

什么是sync.Cond

Go语言标准库中的条件变量sync.Cond,它可以让一组的Goroutine都在满足特定条件时被唤醒。

每个Cond都会关联一个Lock(*sync.Mutex or *sync.RWMutex)

  1. var (
  2. locker = new(sync.Mutex)
  3. cond = sync.NewCond(locker)
  4. )
  5. func listen(x int) {
  6. // 获取锁
  7. cond.L.Lock()
  8. // 等待通知 暂时阻塞
  9. cond.Wait()
  10. fmt.Println(x)
  11. // 释放锁
  12. cond.L.Unlock()
  13. }
  14. func main() {
  15. // 启动60个被cond阻塞的线程
  16. for i := 1; i <= 60; i++ {
  17. go listen(i)
  18. }
  19. fmt.Println("start all")
  20. // 3秒之后 下发一个通知给已经获取锁的goroutine time.Sleep(time.Second * 3)
  21. fmt.Println("++++++++++++++++++++one Signal")
  22. cond.Signal()
  23. // 3秒之后 下发一个通知给已经获取锁的goroutine
  24. time.Sleep(time.Second * 3)
  25. fmt.Println("++++++++++++++++++++one Signal")
  26. cond.Signal()
  27. // 3秒之后 下发广播给所有等待的goroutine
  28. time.Sleep(time.Second * 3)
  29. fmt.Println("++++++++++++++++++++begin broadcast")
  30. cond.Broadcast()
  31. // 阻塞直到所有的全部输出
  32. time.Sleep(time.Second * 60)
  33. }

上面是个简单的例子,我们启动了60个线程,然后都被cond阻塞,主函数通过Signal()通知一个goroutine接触阻塞,通过Broadcast()通知所有被阻塞的全部解除阻塞。

看下源码

  1. // Wait 原子式的 unlock c.L, 并暂停执行调用的 goroutine。
  2. // 在稍后执行后,Wait 会在返回前 lock c.L. 与其他系统不同,
  3. // 除非被 Broadcast 或 Signal 唤醒,否则等待无法返回。
  4. //
  5. // 因为等待第一次 resume 时 c.L 没有被锁定,所以当 Wait 返回时,
  6. // 调用者通常不能认为条件为真。相反,调用者应该在循环中使用 Wait():
  7. //
  8. // c.L.Lock()
  9. // for !condition() {
  10. // c.Wait()
  11. // }
  12. // ... make use of condition ...
  13. // c.L.Unlock()
  14. //
  15. type Cond struct {
  16. // 用于保证结构体不会在编译期间拷贝
  17. noCopy noCopy
  18. // 锁
  19. L Locker
  20. // goroutine链表,维护等待唤醒的goroutine队列
  21. notify notifyList
  22. // 保证运行期间不会发生copy
  23. checker copyChecker
  24. }

重点分析下:notifyListcopyChecker

  • notify
  1. type notifyList struct {
  2. // 总共需要等待的数量
  3. wait uint32
  4. // 已经通知的数量
  5. notify uint32
  6. // 锁
  7. lock uintptr
  8. // 指向链表头部
  9. head *sudog
  10. // 指向链表尾部
  11. tail *sudog
  12. }

这个是核心,所有waitgoroutine都会被加入到这个链表中,然后在通知的时候再从这个链表中获取。

  • copyChecker

保证运行期间不会发生copy

  1. type copyChecker uintptr
  2. // copyChecker holds back pointer to itself to detect object copying
  3. func (c *copyChecker) check() {
  4. if uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
  5. !atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
  6. uintptr(*c) != uintptr(unsafe.Pointer(c)) {
  7. panic("sync.Cond is copied")
  8. }
  9. }

Wait

  1. func (c *Cond) Wait() {
  2. // 监测是否复制
  3. c.checker.check()
  4. // 更新 notifyList中需要等待的wait的数量
  5. // 返回当前需要插入链表节点ticket
  6. t := runtime_notifyListAdd(&c.notify)
  7. c.L.Unlock()
  8. // 为当前的加入的waiter构建一个链表的节点,插入链表的尾部
  9. runtime_notifyListWait(&c.notify, t)
  10. c.L.Lock()
  11. }
  12. // go/src/runtime/sema.go
  13. // 更新 notifyList中需要等待的wait的数量
  14. // 同时返回当前的加入的 waiter 的 ticket 编号,从0开始
  15. //go:linkname notifyListAdd sync.runtime_notifyListAdd
  16. func notifyListAdd(l *notifyList) uint32 {
  17. // 使用atomic原子的对wait字段进行加一操作
  18. return atomic.Xadd(&l.wait, 1) - 1
  19. }
  20. // go/src/runtime/sema.go
  21. // 为当前的加入的waiter构建一个链表的节点,插入链表的尾部
  22. //go:linkname notifyListWait sync.runtime_notifyListWait
  23. func notifyListWait(l *notifyList, t uint32) {
  24. lock(&l.lock)
  25. // 当t小于notifyList中的notify,说明当前节点已经被通知了
  26. if less(t, l.notify) {
  27. unlock(&l.lock)
  28. return
  29. }
  30. // 构建当前节点
  31. s := acquireSudog()
  32. s.g = getg()
  33. s.ticket = t
  34. s.releasetime = 0
  35. t0 := int64(0)
  36. if blockprofilerate > 0 {
  37. t0 = cputicks()
  38. s.releasetime = -1
  39. }
  40. // 头结点没构建,插入头结点
  41. if l.tail == nil {
  42. l.head = s
  43. } else {
  44. // 插入到尾节点
  45. l.tail.next = s
  46. }
  47. l.tail = s
  48. // 将当前goroutine置于等待状态并解锁
  49. // 通过调用goready(gp),可以使goroutine再次可运行。
  50. // 也就是将 M/P/G 解绑,并将 G 调整为等待状态,放入 sudog 等待队列中
  51. goparkunlock(&l.lock, waitReasonSyncCondWait, traceEvGoBlockCond, 3)
  52. if t0 != 0 {
  53. blockevent(s.releasetime-t0, 2)
  54. }
  55. releaseSudog(s)
  56. }

梳理流程

1、首先检测对象的复制行为,如果有复制发生直接抛出panic;

2、然后调用runtime_notifyListAddnotifynotifyListList中的wait(需要等待的数量)进行加一操作,同时返回一个ticket,用来作为当前wait的编号,这个编号,会和notifyList中的notify对应起来;

3、然后调用runtime_notifyListWait把当前的wait封装成链表的一个节点,插入到notifyList维护的链表的尾部。

Signal

  1. // 唤醒一个被wait的goroutine
  2. func (c *Cond) Signal() {
  3. // 监测是否复制
  4. c.checker.check()
  5. runtime_notifyListNotifyOne(&c.notify)
  6. }
  7. // go/src/runtime/sema.go
  8. // 通知链表中的第一个
  9. //go:linkname notifyListNotifyOne sync.runtime_notifyListNotifyOne
  10. func notifyListNotifyOne(l *notifyList) {
  11. // wait和notify,说明已经全部通知到了
  12. if atomic.Load(&l.wait) == atomic.Load(&l.notify) {
  13. return
  14. }
  15. lock(&l.lock)
  16. // 这里做了二次的确认
  17. // wait和notify,说明已经全部通知到了
  18. t := l.notify
  19. if t == atomic.Load(&l.wait) {
  20. unlock(&l.lock)
  21. return
  22. }
  23. // 原子的对notify执行+1操作
  24. atomic.Store(&l.notify, t+1)
  25. // 尝试找到需要被通知的 g
  26. // 如果目前还没来得及入队,是无法找到的
  27. // 但是,当它看到通知编号已经发生改变是不会被 park 的
  28. //
  29. // 这个查找过程看起来是线性复杂度,但实际上很快就停了
  30. // 因为 g 的队列与获取编号不同,因而队列中会出现少量重排,但我们希望找到靠前的 g
  31. // 而 g 只有在不再 race 后才会排在靠前的位置,因此这个迭代也不会太久,
  32. // 同时,即便找不到 g,这个情况也成立:
  33. // 它还没有休眠,并且已经失去了我们在队列上找到的(少数)其他 g 的 race。
  34. for p, s := (*sudog)(nil), l.head; s != nil; p, s = s, s.next {
  35. // 顺序拿到一个节点的ticket,会和上面会和notifyList中的notify做比较,相同才进行后续的操作
  36. // 这个我们分析了,notifyList中的notify和链表节点中的ticket是一一对应的
  37. if s.ticket == t {
  38. n := s.next
  39. if p != nil {
  40. p.next = n
  41. } else {
  42. l.head = n
  43. }
  44. if n == nil {
  45. l.tail = p
  46. }
  47. unlock(&l.lock)
  48. s.next = nil
  49. // 通过goready掉起在上面通过goparkunlock挂起的goroutine
  50. readyWithTime(s, 4)
  51. return
  52. }
  53. }
  54. unlock(&l.lock)
  55. }

梳理下流程:

1、首先检测对象的复制行为,如果有复制发生直接抛出panic

2、判断waitnotify,如果两者相同说明已经已经全部通知到了;

3、调用notifyListNotifyOne,通过for循环,依次遍历这个链表,直到找到和notifyList中的notify,相匹配的ticket的节点;

4、掉起goroutine,完成通知。

Broadcast

  1. // 唤醒所有被wait的goroutine
  2. func (c *Cond) Broadcast() {
  3. c.checker.check()
  4. runtime_notifyListNotifyAll(&c.notify)
  5. }
  6. // go/src/runtime/sema.go
  7. // notifyListNotifyAll notifies all entries in the list.
  8. //go:linkname notifyListNotifyAll sync.runtime_notifyListNotifyAll
  9. func notifyListNotifyAll(l *notifyList) {
  10. // wait和notify,说明已经全部通知到了
  11. if atomic.Load(&l.wait) == atomic.Load(&l.notify) {
  12. return
  13. }
  14. // 加锁
  15. lock(&l.lock)
  16. s := l.head
  17. l.head = nil
  18. l.tail = nil
  19. // 这个很粗暴,直接将notify的值置换成wait
  20. atomic.Store(&l.notify, atomic.Load(&l.wait))
  21. unlock(&l.lock)
  22. // 循环链表,一个个唤醒goroutine
  23. for s != nil {
  24. next := s.next
  25. s.next = nil
  26. readyWithTime(s, 4)
  27. s = next
  28. }
  29. }

梳理下流程:

1、首先检测对象的复制行为,如果有复制发生直接抛出panic;

2、判断waitnotify,如果两者相同说明已经已经全部通知到了;

3、notifyListNotifyAll,就相对简单了,直接将notify的值置为wait,标注这个已经全部通知了;

4、循环链表,一个个唤醒goroutine

总结

sync.Cond不是一个常用的同步机制,但是在条件长时间无法满足时,与使用for {}进行忙碌等待相比,sync.Cond能够让出处理器的使用权,提供CPU的利用率。使用时我们也需要注意以下问题:

1、sync.Cond.Wait在调用之前一定要使用获取互斥锁,否则会触发程序崩溃;

2、sync.Cond.Signal 唤醒的 Goroutine都是队列最前面、等待最久的Goroutine

3、sync.Cond.Broadcast会按照一定顺序广播通知等待的全部 Goroutine

go中sync.Cond源码解读的更多相关文章

  1. go中sync.Mutex源码解读

    互斥锁 前言 什么是sync.Mutex 分析下源码 Lock 位运算 Unlock 总结 参考 互斥锁 前言 本次的代码是基于go version go1.13.15 darwin/amd64 什么 ...

  2. go中sync.Once源码解读

    sync.Once 前言 sync.Once的作用 实现原理 总结 sync.Once 前言 本次的代码是基于go version go1.13.15 darwin/amd64 sync.Once的作 ...

  3. go中semaphore(信号量)源码解读

    运行时信号量机制 semaphore 前言 作用是什么 几个主要的方法 如何实现 sudog 缓存 acquireSudog releaseSudog semaphore poll_runtime_S ...

  4. java jdk 中HashMap的源码解读

    HashMap是我们在日常写代码时最常用到的一个数据结构,它为我们提供key-value形式的数据存储.同时,它的查询,插入效率都非常高. 在之前的排序算法总结里面里,我大致学习了HashMap的实现 ...

  5. Alamofire源码解读系列(四)之参数编码(ParameterEncoding)

    本篇讲解参数编码的内容 前言 我们在开发中发的每一个请求都是通过URLRequest来进行封装的,可以通过一个URL生成URLRequest.那么如果我有一个参数字典,这个参数字典又是如何从客户端传递 ...

  6. JDK容器类Map源码解读

    java.util.Map接口是JDK1.2开始提供的一个基于键值对的散列表接口,其设计的初衷是为了替换JDK1.0中的java.util.Dictionary抽象类.Dictionary是JDK最初 ...

  7. etcd学习(6)-etcd实现raft源码解读

    etcd中raft实现源码解读 前言 raft实现 看下etcd中的raftexample newRaftNode startRaft serveChannels 领导者选举 启动并初始化node节点 ...

  8. 【原】Spark中Job的提交源码解读

    版权声明:本文为原创文章,未经允许不得转载. Spark程序程序job的运行是通过actions算子触发的,每一个action算子其实是一个runJob方法的运行,详见文章 SparkContex源码 ...

  9. HttpServlet中service方法的源码解读

    前言     最近在看<Head First Servlet & JSP>这本书, 对servlet有了更加深入的理解.今天就来写一篇博客,谈一谈Servlet中一个重要的方法-- ...

随机推荐

  1. c# grpc

    刚接触RPC时只知道概念是远程过程调用协议,分为服务端和客户端,客户端请求服务端,服务端再回应客户端,粗看和HTTP一应一答没有什么区别.既然有着存在即合理的说法,网上找找说法,有的讲的太深感觉太啰嗦 ...

  2. 数学知识-欧拉函数&快速幂

    欧拉函数 定义 对于正整数n,欧拉函数是小于或等于n的正整数中与n互质的数的数目,记作φ(n). 算法思路 既然求解每个数的欧拉函数,都需要知道他的质因子,而不需要个数 因此,我们只需求出他的质因子, ...

  3. 国产网络损伤仪 SandStorm -- 只需要拖拽就能删除链路规则

    国产网络损伤仪SandStorm可以模拟出带宽限制.时延.时延抖动.丢包.乱序.重复报文.误码.拥塞等网络状况,在实验室条件下准确可靠地测试出网络应用在真实网络环境中的性能,以帮助应用程序在上线部署前 ...

  4. kubernetes生态--交付prometheus监控及grafana炫酷dashboard到k8s集群

    由于docker容器的特殊性,传统的zabbix无法对k8s集群内的docker状态进行监控,所以需要使用prometheus来进行监控: 什么是Prometheus? Prometheus是由Sou ...

  5. Java RMI 实现一个简单的GFS(谷歌文件系统)——介绍篇

    本系列主要是使用Java RMI实现一个简单的GFS(谷歌文件系统,google file system),首先整体简单介绍下该项目. [为了更好的阅读以及查看其他篇章,请查看原文:https://w ...

  6. DC1(msf drupal7+suid-find提权)

    这边我们靶机是仅主机模式,IP是192.168.56.101,,直接上msf拿到shell,  不过payload要改一下 改成php/meterperter/bind_tcp 拿到shell了 ,采 ...

  7. javascript questions & code review

    javascript questions & code review refs https://github.com/learning-js-by-reading-source-codes/j ...

  8. Python Quiz & Python Exercise

    Python Quiz & Python Exercise https://www.w3schools.com/quiztest/quiztest.asp?qtest=PYTHON https ...

  9. js types & primitive & object

    js types & primitive & object js 数据类型 typeof null // "object" typeof undefined // ...

  10. AMP & PWA

    AMP & PWA AMP is a web component framework to easily create user-first websites. stories. ads. e ...