go中sync.Cond源码解读
sync.Cond
前言
本次的代码是基于go version go1.13.15 darwin/amd64
什么是sync.Cond
Go语言标准库中的条件变量sync.Cond
,它可以让一组的Goroutine
都在满足特定条件时被唤醒。
每个Cond
都会关联一个Lock(*sync.Mutex or *sync.RWMutex)
var (
locker = new(sync.Mutex)
cond = sync.NewCond(locker)
)
func listen(x int) {
// 获取锁
cond.L.Lock()
// 等待通知 暂时阻塞
cond.Wait()
fmt.Println(x)
// 释放锁
cond.L.Unlock()
}
func main() {
// 启动60个被cond阻塞的线程
for i := 1; i <= 60; i++ {
go listen(i)
}
fmt.Println("start all")
// 3秒之后 下发一个通知给已经获取锁的goroutine time.Sleep(time.Second * 3)
fmt.Println("++++++++++++++++++++one Signal")
cond.Signal()
// 3秒之后 下发一个通知给已经获取锁的goroutine
time.Sleep(time.Second * 3)
fmt.Println("++++++++++++++++++++one Signal")
cond.Signal()
// 3秒之后 下发广播给所有等待的goroutine
time.Sleep(time.Second * 3)
fmt.Println("++++++++++++++++++++begin broadcast")
cond.Broadcast()
// 阻塞直到所有的全部输出
time.Sleep(time.Second * 60)
}
上面是个简单的例子,我们启动了60个线程,然后都被cond
阻塞,主函数通过Signal()
通知一个goroutine
接触阻塞,通过Broadcast()
通知所有被阻塞的全部解除阻塞。
看下源码
// Wait 原子式的 unlock c.L, 并暂停执行调用的 goroutine。
// 在稍后执行后,Wait 会在返回前 lock c.L. 与其他系统不同,
// 除非被 Broadcast 或 Signal 唤醒,否则等待无法返回。
//
// 因为等待第一次 resume 时 c.L 没有被锁定,所以当 Wait 返回时,
// 调用者通常不能认为条件为真。相反,调用者应该在循环中使用 Wait():
//
// c.L.Lock()
// for !condition() {
// c.Wait()
// }
// ... make use of condition ...
// c.L.Unlock()
//
type Cond struct {
// 用于保证结构体不会在编译期间拷贝
noCopy noCopy
// 锁
L Locker
// goroutine链表,维护等待唤醒的goroutine队列
notify notifyList
// 保证运行期间不会发生copy
checker copyChecker
}
重点分析下:notifyList
和copyChecker
- notify
type notifyList struct {
// 总共需要等待的数量
wait uint32
// 已经通知的数量
notify uint32
// 锁
lock uintptr
// 指向链表头部
head *sudog
// 指向链表尾部
tail *sudog
}
这个是核心,所有wait
的goroutine
都会被加入到这个链表中,然后在通知的时候再从这个链表中获取。
- copyChecker
保证运行期间不会发生copy
type copyChecker uintptr
// copyChecker holds back pointer to itself to detect object copying
func (c *copyChecker) check() {
if uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
!atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
uintptr(*c) != uintptr(unsafe.Pointer(c)) {
panic("sync.Cond is copied")
}
}
Wait
func (c *Cond) Wait() {
// 监测是否复制
c.checker.check()
// 更新 notifyList中需要等待的wait的数量
// 返回当前需要插入链表节点ticket
t := runtime_notifyListAdd(&c.notify)
c.L.Unlock()
// 为当前的加入的waiter构建一个链表的节点,插入链表的尾部
runtime_notifyListWait(&c.notify, t)
c.L.Lock()
}
// go/src/runtime/sema.go
// 更新 notifyList中需要等待的wait的数量
// 同时返回当前的加入的 waiter 的 ticket 编号,从0开始
//go:linkname notifyListAdd sync.runtime_notifyListAdd
func notifyListAdd(l *notifyList) uint32 {
// 使用atomic原子的对wait字段进行加一操作
return atomic.Xadd(&l.wait, 1) - 1
}
// go/src/runtime/sema.go
// 为当前的加入的waiter构建一个链表的节点,插入链表的尾部
//go:linkname notifyListWait sync.runtime_notifyListWait
func notifyListWait(l *notifyList, t uint32) {
lock(&l.lock)
// 当t小于notifyList中的notify,说明当前节点已经被通知了
if less(t, l.notify) {
unlock(&l.lock)
return
}
// 构建当前节点
s := acquireSudog()
s.g = getg()
s.ticket = t
s.releasetime = 0
t0 := int64(0)
if blockprofilerate > 0 {
t0 = cputicks()
s.releasetime = -1
}
// 头结点没构建,插入头结点
if l.tail == nil {
l.head = s
} else {
// 插入到尾节点
l.tail.next = s
}
l.tail = s
// 将当前goroutine置于等待状态并解锁
// 通过调用goready(gp),可以使goroutine再次可运行。
// 也就是将 M/P/G 解绑,并将 G 调整为等待状态,放入 sudog 等待队列中
goparkunlock(&l.lock, waitReasonSyncCondWait, traceEvGoBlockCond, 3)
if t0 != 0 {
blockevent(s.releasetime-t0, 2)
}
releaseSudog(s)
}
梳理流程
1、首先检测对象的复制行为,如果有复制发生直接抛出panic;
2、然后调用runtime_notifyListAdd
对notifynotifyListList
中的wait
(需要等待的数量)进行加一操作,同时返回一个ticket
,用来作为当前wait
的编号,这个编号,会和notifyList
中的notify
对应起来;
3、然后调用runtime_notifyListWait
把当前的wait
封装成链表的一个节点,插入到notifyList
维护的链表的尾部。
Signal
// 唤醒一个被wait的goroutine
func (c *Cond) Signal() {
// 监测是否复制
c.checker.check()
runtime_notifyListNotifyOne(&c.notify)
}
// go/src/runtime/sema.go
// 通知链表中的第一个
//go:linkname notifyListNotifyOne sync.runtime_notifyListNotifyOne
func notifyListNotifyOne(l *notifyList) {
// wait和notify,说明已经全部通知到了
if atomic.Load(&l.wait) == atomic.Load(&l.notify) {
return
}
lock(&l.lock)
// 这里做了二次的确认
// wait和notify,说明已经全部通知到了
t := l.notify
if t == atomic.Load(&l.wait) {
unlock(&l.lock)
return
}
// 原子的对notify执行+1操作
atomic.Store(&l.notify, t+1)
// 尝试找到需要被通知的 g
// 如果目前还没来得及入队,是无法找到的
// 但是,当它看到通知编号已经发生改变是不会被 park 的
//
// 这个查找过程看起来是线性复杂度,但实际上很快就停了
// 因为 g 的队列与获取编号不同,因而队列中会出现少量重排,但我们希望找到靠前的 g
// 而 g 只有在不再 race 后才会排在靠前的位置,因此这个迭代也不会太久,
// 同时,即便找不到 g,这个情况也成立:
// 它还没有休眠,并且已经失去了我们在队列上找到的(少数)其他 g 的 race。
for p, s := (*sudog)(nil), l.head; s != nil; p, s = s, s.next {
// 顺序拿到一个节点的ticket,会和上面会和notifyList中的notify做比较,相同才进行后续的操作
// 这个我们分析了,notifyList中的notify和链表节点中的ticket是一一对应的
if s.ticket == t {
n := s.next
if p != nil {
p.next = n
} else {
l.head = n
}
if n == nil {
l.tail = p
}
unlock(&l.lock)
s.next = nil
// 通过goready掉起在上面通过goparkunlock挂起的goroutine
readyWithTime(s, 4)
return
}
}
unlock(&l.lock)
}
梳理下流程:
1、首先检测对象的复制行为,如果有复制发生直接抛出panic
;
2、判断wait
和notify
,如果两者相同说明已经已经全部通知到了;
3、调用notifyListNotifyOne
,通过for循环,依次遍历这个链表,直到找到和notifyList
中的notify
,相匹配的ticket
的节点;
4、掉起goroutine
,完成通知。
Broadcast
// 唤醒所有被wait的goroutine
func (c *Cond) Broadcast() {
c.checker.check()
runtime_notifyListNotifyAll(&c.notify)
}
// go/src/runtime/sema.go
// notifyListNotifyAll notifies all entries in the list.
//go:linkname notifyListNotifyAll sync.runtime_notifyListNotifyAll
func notifyListNotifyAll(l *notifyList) {
// wait和notify,说明已经全部通知到了
if atomic.Load(&l.wait) == atomic.Load(&l.notify) {
return
}
// 加锁
lock(&l.lock)
s := l.head
l.head = nil
l.tail = nil
// 这个很粗暴,直接将notify的值置换成wait
atomic.Store(&l.notify, atomic.Load(&l.wait))
unlock(&l.lock)
// 循环链表,一个个唤醒goroutine
for s != nil {
next := s.next
s.next = nil
readyWithTime(s, 4)
s = next
}
}
梳理下流程:
1、首先检测对象的复制行为,如果有复制发生直接抛出panic;
2、判断wait
和notify
,如果两者相同说明已经已经全部通知到了;
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源码解读的更多相关文章
- go中sync.Mutex源码解读
互斥锁 前言 什么是sync.Mutex 分析下源码 Lock 位运算 Unlock 总结 参考 互斥锁 前言 本次的代码是基于go version go1.13.15 darwin/amd64 什么 ...
- go中sync.Once源码解读
sync.Once 前言 sync.Once的作用 实现原理 总结 sync.Once 前言 本次的代码是基于go version go1.13.15 darwin/amd64 sync.Once的作 ...
- go中semaphore(信号量)源码解读
运行时信号量机制 semaphore 前言 作用是什么 几个主要的方法 如何实现 sudog 缓存 acquireSudog releaseSudog semaphore poll_runtime_S ...
- java jdk 中HashMap的源码解读
HashMap是我们在日常写代码时最常用到的一个数据结构,它为我们提供key-value形式的数据存储.同时,它的查询,插入效率都非常高. 在之前的排序算法总结里面里,我大致学习了HashMap的实现 ...
- Alamofire源码解读系列(四)之参数编码(ParameterEncoding)
本篇讲解参数编码的内容 前言 我们在开发中发的每一个请求都是通过URLRequest来进行封装的,可以通过一个URL生成URLRequest.那么如果我有一个参数字典,这个参数字典又是如何从客户端传递 ...
- JDK容器类Map源码解读
java.util.Map接口是JDK1.2开始提供的一个基于键值对的散列表接口,其设计的初衷是为了替换JDK1.0中的java.util.Dictionary抽象类.Dictionary是JDK最初 ...
- etcd学习(6)-etcd实现raft源码解读
etcd中raft实现源码解读 前言 raft实现 看下etcd中的raftexample newRaftNode startRaft serveChannels 领导者选举 启动并初始化node节点 ...
- 【原】Spark中Job的提交源码解读
版权声明:本文为原创文章,未经允许不得转载. Spark程序程序job的运行是通过actions算子触发的,每一个action算子其实是一个runJob方法的运行,详见文章 SparkContex源码 ...
- HttpServlet中service方法的源码解读
前言 最近在看<Head First Servlet & JSP>这本书, 对servlet有了更加深入的理解.今天就来写一篇博客,谈一谈Servlet中一个重要的方法-- ...
随机推荐
- OPENSOURCE - libcurl
本文仅做备份存档,原文地址如下,请点链接进入 https://www.cnblogs.com/moodlxs/archive/2012/10/15/2724318.html https://www.c ...
- LINUX - 寄存器和堆栈
堆栈模型: 函数调用: EBP:ESP EBP当前调用函数的栈底: ESP当前调用函数的栈顶: ---------------------------------------------------- ...
- Leetcode(868)-二进制间距
给定一个正整数 N,找到并返回 N 的二进制表示中两个连续的 1 之间的最长距离. 如果没有两个连续的 1,返回 0 . 示例 1: 输入:22 输出:2 解释: 22 的二进制是 0b10110 . ...
- 秋招C++面试相关总结索引
C++相关 C++ part1 C++ part2 C++ part3 C++ part4 C++ part5 C++ part6 C++ part6.5 C++ part7 C++ part8 C+ ...
- Apple CSS Animation All In One
Apple CSS Animation All In One Apple Watch CSS Animation https://codepen.io/xgqfrms/pen/LYZaNMb See ...
- webpack defineConstants
webpack defineConstants PAGES 全局常量/全局变量 https://webpack.js.org/plugins/define-plugin/ taro https://n ...
- true && number !== boolean
true && number !== boolean bug let result = ``; // section, name ? create text, compute cent ...
- vue & template & v-else & v-for bug
vue & template & v-else & v-for bug nested table bug https://codepen.io/xgqfrms/pen/wvaG ...
- DAPHNE PATEL:有主见的人,才能活出精彩人生
有主见的人,会活出什么样子呢?近日,NGK灵石团队技术副总裁DAPHNE 女士给出了答案. DAPHNE PATEL表示,有主见的人,才能活出精彩的人生.为什么这么说呢? DAPHNE PATEL用自 ...
- BGV崛起带动DeFi重回大众视野
自10月份比特币二次发力以来,DeFi越来越被市场忽略,这当然也有比特币给力和DeFi低迷的双重原因,但随着Baccarat的平台币BGV于A网的正式上线,近期DeFi重新回到了大众的视野中. 正如区 ...