Go 语言核心 36 讲
郝林
《Go 并发编程实战》作者,前轻松筹大数据负责人
79610 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 55 讲
Go 语言核心 36 讲
15
15
1.0x
00:00/00:00
登录|注册

28 | 条件变量sync.Cond (下)

唤醒goroutine后重新锁定互斥锁
让当前的goroutine处于等待状态
解锁当前的互斥锁
把调用它的goroutine加入到通知队列
最好在解锁互斥锁之后调用
不需要在互斥锁的保护下执行
最好使用for语句来检查共享资源的状态
需要在互斥锁保护下执行
需要sync.Locker类型的参数值
使用sync.NewCond函数
需要唤醒所有等待的goroutine,使用Broadcast方法
确定只有一个goroutine在等待通知,或者只需唤醒任意一个goroutine,使用Signal方法
Broadcast方法唤醒所有等待的goroutine
Signal方法只唤醒一个等待的goroutine
保险起见,再次调用Wait方法并继续等待下次通知的到来
for语句可以做多次检查
Wait方法内部机制
sync.Cond类型中的公开字段L
通知具有即时性
Signal方法和Broadcast方法
Wait方法
初始化条件变量
适用场景
异同
为什么要用for语句来包裹调用其Wait方法的表达式,用if语句不行吗?
为什么先要锁定条件变量基于的互斥锁,才能调用它的Wait方法?
思考题
总结
问题2:条件变量的Signal方法和Broadcast方法
问题1:条件变量的Wait方法
条件变量sync.Cond

该思维导图由 AI 生成,仅供参考

你好,我是郝林,今天我继续分享条件变量 sync.Cond 的内容。我们紧接着上一篇的内容进行知识扩展。

问题 1:条件变量的Wait方法做了什么?

在了解了条件变量的使用方式之后,你可能会有这么几个疑问。
为什么先要锁定条件变量基于的互斥锁,才能调用它的Wait方法?
为什么要用for语句来包裹调用其Wait方法的表达式,用if语句不行吗?
这些问题我在面试的时候也经常问。你需要对这个Wait方法的内部机制有所了解才能回答上来。
条件变量的Wait方法主要做了四件事。
把调用它的 goroutine(也就是当前的 goroutine)加入到当前条件变量的通知队列中。
解锁当前的条件变量基于的那个互斥锁。
让当前的 goroutine 处于等待状态,等到通知到来时再决定是否唤醒它。此时,这个 goroutine 就会阻塞在调用这个Wait方法的那行代码上。
如果通知到来并且决定唤醒这个 goroutine,那么就在唤醒它之后重新锁定当前条件变量基于的互斥锁。自此之后,当前的 goroutine 就会继续执行后面的代码了。
你现在知道我刚刚说的第一个疑问的答案了吗?
因为条件变量的Wait方法在阻塞当前的 goroutine 之前,会解锁它基于的互斥锁,所以在调用该Wait方法之前,我们必须先锁定那个互斥锁,否则在调用这个Wait方法时,就会引发一个不可恢复的 panic。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

条件变量sync.Cond是Go语言中基于互斥锁的一种同步工具。文章首先解答了条件变量的Wait方法的内部机制,强调了在调用Wait方法前必须先锁定条件变量基于的互斥锁,并使用for语句包裹Wait方法的调用以重复检查共享资源的状态。其次,文章对比了条件变量的Signal方法和Broadcast方法的异同,指出Signal方法只唤醒一个等待通知的goroutine,而Broadcast方法则唤醒所有等待通知的goroutine。此外,还强调了这两个方法不需要受到互斥锁的保护,并提醒条件变量的通知具有即时性。最后,文章提出了一个思考题,探讨了sync.Cond类型中的公开字段L的作用以及是否可以在使用条件变量的过程中改变这个字段的值。整体而言,文章内容涵盖了条件变量的基本使用方法和注意事项,适合读者快速了解条件变量的概述。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Go 语言核心 36 讲》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(45)

  • 最新
  • 精选
  • 不记年
    老师,我有一个疑问,对于cond来说,每次只唤醒一个goruntine,如果这么goruntine发现消息不是自己想要的就会从新阻塞在wait函数中,那么真正需要这个消息的goruntine还会被唤醒吗?

    作者回复: 不会,所以我才说应该优先用 broadcast。

    2019-07-24
    20
  • Laughing
    L公开变量代表cond初始化时传递进来的锁,这个锁的状态是可以改变的,但会影响cond对互斥锁的控制。

    作者回复: 动这个L之前一定要三思,谨慎些,想想是不是会影响到程序。

    2018-10-15
    15
  • Lywane
    ``` lock.Lock() for mailbox == 1 { sendCond.Wait() } mailbox = 1 lock.Unlock() recvCond.Signal() ``` 只要共享资源的状态不变,即使当前的 goroutine 因收到通知而被唤醒,也依然会再次执行这个Wait方法,并再次被阻塞。 老师我对这句话有个疑问,假设Wait不解锁,直接阻塞了当前goroutine,那么当收到通知时,mailbox的值应该已经被改成0了,此时唤醒,应该不满足for循环条件了呀,为什么会再次执行Wait方法呢? 我思考的结论是:Wait解锁是为了让其他goroutine去修改mailbox的值,不知道这么理解对吗。

    作者回复: 首先还是要明确这个过程:sendCond.Wait() 会先解锁再阻塞当前的 goroutine,然后等到别的地方调用 sendCond.Signal() 后,这里的 sendCond.Wait() 会加锁并唤醒当前的 goroutine。 假设有多个 goroutine 都调用了 sendCond.Wait() 方法,如果它们所在的 goroutine 都因条件变量的通知而被唤醒,那么(由于锁的缘故)只有一个 goroutine (以下称“1号G”)能够率先再次检查 mailbox。 “1号G”发现 mailbox 的值已经不是 1 了,随即退出循环,并且又把 mailbox 的值设置成了 1。最后解锁,并给 recvCond 发通知。 由于“1号G”的解锁,“2号G”、“3号G”等等(其他调用了 sendCond.Wait() 方法的G)都会竞争这个锁。得到该锁控制权的某个G会再锁住这个锁,然后发现 mailbox 的值依然是 1,随即再次调用 sendCond.Wait() 方法,并继续等待这个条件变量的下一个通知。其他的G也是类似的。 这个场景和过程要想清楚。可以自己画几条平行线,然后模拟一下这个过程。

    2020-03-27
    5
  • Geek_a8be59
    老实您好,不太明白为什么一定要用条件变量呢。我看了 go并发编程第2版和你这边的做了对应,书上是已生成者消费者为例,我想的是用两个互斥量不行么?生成一个锁,消费一个锁,只是说可能会浪费在循环判断是否可生成,是否可消费中,还有可能因为某些不成功导致一直不能解锁的状况。 难不成条件变量主要就是优化上述问题的么?

    作者回复: 我记得在书里也说过类似的内容:条件变量常用于对共享资源的状态变化的监控和响应(由此可以实现状态机),同时也比较适合帮助多个线程协作操作同一个共享资源(这里的关键词是协作)。 条件变量有两种通知机制,这是互斥量没有的。互斥量只能保护共享资源(这里的关键词是保护),功能比较单一。所以条件变量在一些场景下会更高效。 你自己也说出了一些使用两个互斥锁来做的弊端。这些弊端其实就已经比较闹心了。一个是用两个锁对性能的影响,一个是两个锁如果配合不当就会造成局部的死锁。这还是多方协作操作同一个共享资源中最简单的情况。 再加上我前面说的那些,条件变量在此类场景下的优势已经非常明显了。注意它在Wait时还有原子级的操作呢。

    2019-08-12
    2
  • 樂文💤
    所以如果用的signal方法只通知一个go routine的话 条件变量的判断方法改成if应该是没问题的 但如果是多个go routine 同时被唤醒就可能导致多个go routine 在不满足唤醒状态的时候被唤醒而导致处理错误,这时候就必须用for来不断地进行检测可以这么理解么

    作者回复: 总是需要用 for 的,因为你没法保证收到信号之后那个状态就一定是你想要的。

    2019-08-03
    3
    2
  • 手指饼干
    老师您好,关于发送通知是否需要在锁的保护下调用,还是有些疑问,以下想法请老师帮忙看看是否理解正确: 一个在等待通知的 goroutine 收到通知信号被唤醒,接下来执行的是条件变量 c.L.Lock() 操作,无论信号的发送方是否是在锁的保护下发送信号,该 goroutine 已经不是在等待通知的状态了,而是在尝试获取锁的状态,即使被阻塞,也是因为获取不到锁。区别只是,如果信号发送方在 Unlock() 之后发送信号,那么该 goroutine 被唤醒后获得锁可能会衔接得更好一点。 对于某些场景,比如说函数的开头两行代码就是 mutex.Lock() defer mutex.Unlock() 这种情况是否可以允许通知在 Unlock() 之前调用

    作者回复: 首先要明确: 条件变量的这两个方法并不需要受到互斥锁的保护,我们也最好不要在解锁互斥锁之前调用它们。 其次: 等待方法对锁的操作主要是为了让多个等待方有秩序的行事。在真正等待前释放锁,是为了让其他的等待方也具有进入等待状态的条件。在唤醒之后再次试图持有锁,是为了让多个等待方串行地检查条件是否满足以及执行后续的操作。 所以,结论是: 发送方在发送通知的时候最好不要持有等待方操作的那个锁。而等待方,应该在等待的时候持有锁加以保护。 最后: 关于你的最后一个问题,如果 mutex 是等待方操作的那个锁,那么就有可能产生发送方与等待方的互斥问题。由于等待方会在真正等待之前释放锁,所以应该不会产生完全死锁的情况。但是,这种没必要的操作肯定会影响程序的效率。 如果 mutex 与等待方没关系,那就无所谓了。你可以根据“是否想串行化通知的发送操作”来决定是否加锁。不过这通常也没什么必要。

    2020-08-01
    1
  • 黄仲辉
    sync.crod 类比 java的 监视器锁

    作者回复: sync.Cond其实对应的是操作系统API中的条件变量。我记得Java也有条件变量这个东西吧。

    2022-09-04归属地:北京
  • Geek_108cb5
    试了一下把互斥锁改成读写锁, 中途会发生一个发送消息被两个接受者同时收到的场景, 导致最后发送者阻塞, 此时主线程的信道也阻塞了, 接受者协程执行完毕, 整体是死锁的情况。 那假如发送者和接收者都是无限循环, 而且多个接收者接收者接到同一份消息不会对业务有影响的情况下, 使用读写锁应该也是没有问题的?

    作者回复: 如果你用的是读写锁的话,读写锁中的读锁定操作之间是不互斥的。所以我估计是你这块的用法错了。

    2022-03-01
  • HiNeNi
    func testCond() { mu := sync.Mutex{} cond := sync.NewCond(&mu) s := "" go func () { fmt.Println("This is consumer") for { mu.Lock() for len(s) == 0 { cond.Wait() } //time.Sleep(time.Second * 1) fmt.Println("consumer, s:", s) s = "" mu.Unlock() cond.Signal() } }() go func () { fmt.Println("This is producer") for { mu.Lock() for len(s) > 0 { cond.Wait() } fmt.Println("produce a string") s = "generate s resource" mu.Unlock() cond.Signal() } }() time.Sleep(time.Second * 10) } ========================================== 自己测的时候发现用一个Cond就可以满足生产者消费者的简单模型,谁能告诉我为什么例子一定要使用两个Cond?

    作者回复: 因为核心需求不同。在你的例子只需要纯粹单向的生产-消费(这就是最简单的情况)。但在专栏的例子里,双方属于“秘密接触”,需要尽可能少的“露面”和“访问邮箱”。所以才需要互相通知。 需求决定代码。

    2021-11-11
  • 传说中的成大大
    看了你下面的留言 感觉条件变量主要是为了避免互斥锁或者读写锁 锁竞争条件

    作者回复: 条件变量的特色是“协调”,而不是“互斥”。但是你也看到了,它是基于锁的。

    2020-04-07
收起评论
显示
设置
留言
45
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部