28 | 条件变量sync.Cond (下)
28 | 条件变量sync.Cond (下)
讲述:黄洲君
时长08:23大小3.84M
问题 1:条件变量的Wait方法做了什么?
问题 2:条件变量的Signal方法和Broadcast方法有哪些异同?
总结
思考题
赞 19
提建议
精选留言(45)
- 郝林2019-06-18对条件变量这个工具本身还有疑问的读者可以去看我写的《Go 并发编程实战》第二版。这本书从并发程序的基本概念讲起,用一定篇幅的图文内容详细地讲解了条件变量的用法,同时还有一个贯穿了互斥锁和条件变量的示例。由于这本书的版权在出版社那里,所以我不能把其中的内容搬到这里。 我在这里只对大家共同的疑问进行简要说明: 1. 条件变量适合保护那些可执行两个对立操作的共享资源。比如,一个既可读又可写的共享文件。又比如,既有生产者又有消费者的产品池。 2. 对于有着对立操作的共享资源(比如一个共享文件),我们通常需要基于同一个锁的两个条件变量(比如 rcond 和 wcond)分别保护读操作和写操作(比如 rcond 保护读,wcond 保护写)。而且,读操作和写操作都需要同时持有这两个条件变量。因为,读操作在操作完成后还要向 wcond 发通知;写操作在操作完成后还要向 rcond 发通知。如此一来,读写操作才能在较少的锁争用的情况下交替进行。 3. 对于同一个条件变量,我们在调用它的 Signal 方法和 Broadcast 方法的时候不应该处在其包含的那个锁的保护下。也就是说,我们应该先撤掉保护屏障,再向 Wait 方法的调用方发出通知。否则,Wait 方法的调用方就有可能会错过通知。这也是我更推荐使用 Broadcast 方法的原因。所有等待方都错过通知的概率要小很多。 4. 相对应的,我们在调用条件变量的 Wait 方法的时候,应该处在其中的锁的保护之下。因为有同一个锁保护,所以不可能有多个 goroutine 同时执行到这个 Wait 方法调用,也就不可能存在针对其中锁的重复解锁。 5. 再强调一下。对于同一个锁,多个 goroutine 对它重复锁定时只会有一个成功,其余的会阻塞;多个 goroutine 对它重复解锁时也只会有一个成功,但其余的会抛 panic。展开共 3 条评论42
- Geek_14c5582019-05-28我理解是这样的。流程: 外部函数加锁 -> 判断条件变量->wait内部解锁->阻塞等待信号->wait内部加锁-> 修改条件变量-> 外部解锁-> 触发信号。 第一次加解锁是为了保证读条件变量时它不会被修改, wait解锁是为了条件变量能够被其他线程改变。wait内部再次加锁,是对条件变量的保护,因为外部要修改。共 1 条评论20
- 不记年2019-07-24老师,我有一个疑问,对于cond来说,每次只唤醒一个goruntine,如果这么goruntine发现消息不是自己想要的就会从新阻塞在wait函数中,那么真正需要这个消息的goruntine还会被唤醒吗?
作者回复: 不会,所以我才说应该优先用 broadcast。
16 - Laughing2018-10-15L公开变量代表cond初始化时传递进来的锁,这个锁的状态是可以改变的,但会影响cond对互斥锁的控制。
作者回复: 动这个L之前一定要三思,谨慎些,想想是不是会影响到程序。
12 - 云学2018-10-18有个疑问,broadcast唤醒所有wait的goroutine,那他们被唤醒时需要去加锁(wait返回),都能成功吗?共 2 条评论8
- 猫王者2018-10-23“我们最好在解锁条件变量基于的那个互斥锁之后,再去调用它的这两个方法(signal和Broadcast)。这更有利于程序的运行效率” 这个应该如何理解? 我的理解是如果先调用signal方法,然后在unlock解锁,如果在这两个操作中间该线程失去cpu,或者我人为的在siganl和unlock之间调用time.Sleep();在另一个等待线程中即使该等待线程被前者所发出的signal唤醒,但是唤醒的时候同时会去进行lock操作,但是前者的线程中由于失去了cpu,并没有调用unlock,那么这次唤醒不是应该失败了吗,即使前者有得到了cpu去执行了unlcok,但是signal操作具有及时性,等待线程不是应该继续等待下一个signal吗,感觉最后会变成死锁啊展开共 2 条评论5
- 打你2018-11-10在看了一遍,清楚了4
- Lywane2020-03-27``` 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也是类似的。 这个场景和过程要想清楚。可以自己画几条平行线,然后模拟一下这个过程。
3 - 樂文💤2019-08-03所以如果用的signal方法只通知一个go routine的话 条件变量的判断方法改成if应该是没问题的 但如果是多个go routine 同时被唤醒就可能导致多个go routine 在不满足唤醒状态的时候被唤醒而导致处理错误,这时候就必须用for来不断地进行检测可以这么理解么
作者回复: 总是需要用 for 的,因为你没法保证收到信号之后那个状态就一定是你想要的。
共 3 条评论2 - 甦2018-12-13源码问题问一下郝老师,cond wait方法里,多个协程走到c.L.Unlock()那一步不会出问题吗? 只有一个协程可以unlock成功,其他协程重复unlock不就panic了吗?共 2 条评论2
- 打你2018-11-10lock.Lock() for mailbox == 1 { sendCond.Wait() } mailbox = 1 lock.Unlock() recvCond.Signal() 如果wait已经解锁lock.Lock()锁住的锁,后面lock.Unlock解锁是什么意思?不会panic。 条件变量这2篇看起来前后理解不到展开共 3 条评论2
- 手指饼干2020-08-01老师您好,关于发送通知是否需要在锁的保护下调用,还是有些疑问,以下想法请老师帮忙看看是否理解正确: 一个在等待通知的 goroutine 收到通知信号被唤醒,接下来执行的是条件变量 c.L.Lock() 操作,无论信号的发送方是否是在锁的保护下发送信号,该 goroutine 已经不是在等待通知的状态了,而是在尝试获取锁的状态,即使被阻塞,也是因为获取不到锁。区别只是,如果信号发送方在 Unlock() 之后发送信号,那么该 goroutine 被唤醒后获得锁可能会衔接得更好一点。 对于某些场景,比如说函数的开头两行代码就是 mutex.Lock() defer mutex.Unlock() 这种情况是否可以允许通知在 Unlock() 之前调用展开
作者回复: 首先要明确: 条件变量的这两个方法并不需要受到互斥锁的保护,我们也最好不要在解锁互斥锁之前调用它们。 其次: 等待方法对锁的操作主要是为了让多个等待方有秩序的行事。在真正等待前释放锁,是为了让其他的等待方也具有进入等待状态的条件。在唤醒之后再次试图持有锁,是为了让多个等待方串行地检查条件是否满足以及执行后续的操作。 所以,结论是: 发送方在发送通知的时候最好不要持有等待方操作的那个锁。而等待方,应该在等待的时候持有锁加以保护。 最后: 关于你的最后一个问题,如果 mutex 是等待方操作的那个锁,那么就有可能产生发送方与等待方的互斥问题。由于等待方会在真正等待之前释放锁,所以应该不会产生完全死锁的情况。但是,这种没必要的操作肯定会影响程序的效率。 如果 mutex 与等待方没关系,那就无所谓了。你可以根据“是否想串行化通知的发送操作”来决定是否加锁。不过这通常也没什么必要。
1 - Geek_a8be592019-08-12老实您好,不太明白为什么一定要用条件变量呢。我看了 go并发编程第2版和你这边的做了对应,书上是已生成者消费者为例,我想的是用两个互斥量不行么?生成一个锁,消费一个锁,只是说可能会浪费在循环判断是否可生成,是否可消费中,还有可能因为某些不成功导致一直不能解锁的状况。 难不成条件变量主要就是优化上述问题的么?
作者回复: 我记得在书里也说过类似的内容:条件变量常用于对共享资源的状态变化的监控和响应(由此可以实现状态机),同时也比较适合帮助多个线程协作操作同一个共享资源(这里的关键词是协作)。 条件变量有两种通知机制,这是互斥量没有的。互斥量只能保护共享资源(这里的关键词是保护),功能比较单一。所以条件变量在一些场景下会更高效。 你自己也说出了一些使用两个互斥锁来做的弊端。这些弊端其实就已经比较闹心了。一个是用两个锁对性能的影响,一个是两个锁如果配合不当就会造成局部的死锁。这还是多方协作操作同一个共享资源中最简单的情况。 再加上我前面说的那些,条件变量在此类场景下的优势已经非常明显了。注意它在Wait时还有原子级的操作呢。
1 - RegExp2019-03-30条件变量的Wait方法在阻塞当前的 goroutine之前,会解锁它基于的互斥锁,那是不是显示调用lock.Lock()的锁也被解锁了呢?
作者回复: 原理上是这样,但是实践中最好不要这样混用同一个锁。
1 - daydaygo2022-11-13 来自上海看完虽然了解了 syncCond 的用法, 但是使用场景还是不了解, 特别是从来没遇到过, 更感觉这个东西「无用」1
- 黄仲辉2022-09-04 来自北京sync.crod 类比 java的 监视器锁
作者回复: sync.Cond其实对应的是操作系统API中的条件变量。我记得Java也有条件变量这个东西吧。
- Geek_108cb52022-03-01试了一下把互斥锁改成读写锁, 中途会发生一个发送消息被两个接受者同时收到的场景, 导致最后发送者阻塞, 此时主线程的信道也阻塞了, 接受者协程执行完毕, 整体是死锁的情况。 那假如发送者和接收者都是无限循环, 而且多个接收者接收者接到同一份消息不会对业务有影响的情况下, 使用读写锁应该也是没有问题的?
作者回复: 如果你用的是读写锁的话,读写锁中的读锁定操作之间是不互斥的。所以我估计是你这块的用法错了。
- 李二木2022-02-22条件变量就是解决并发中的同步问题,原理跟Java差不多。
- 胡小涵2021-11-11func 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?展开
作者回复: 因为核心需求不同。在你的例子只需要纯粹单向的生产-消费(这就是最简单的情况)。但在专栏的例子里,双方属于“秘密接触”,需要尽可能少的“露面”和“访问邮箱”。所以才需要互相通知。 需求决定代码。
- 传说中的成大大2020-04-07看了你下面的留言 感觉条件变量主要是为了避免互斥锁或者读写锁 锁竞争条件
作者回复: 条件变量的特色是“协调”,而不是“互斥”。但是你也看到了,它是基于锁的。