• 郝林
    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。
    展开
     1
     11
  • 云学
    2018-10-18
    有个疑问,broadcast唤醒所有wait的goroutine,那他们被唤醒时需要去加锁(wait返回),都能成功吗?
     2
     6
  • Geek_14c558
    2019-05-28
    我理解是这样的。流程: 外部函数加锁 -> 判断条件变量->wait内部解锁->阻塞等待信号->wait内部加锁-> 修改条件变量-> 外部解锁-> 触发信号。 第一次加解锁是为了保证读条件变量时它不会被修改, wait解锁是为了条件变量能够被其他线程改变。wait内部再次加锁,是对条件变量的保护,因为外部要修改。
     1
     5
  • 猫王者
    2018-10-23
    “我们最好在解锁条件变量基于的那个互斥锁之后,再去调用它的这两个方法(signal和Broadcast)。这更有利于程序的运行效率” 这个应该如何理解?

    我的理解是如果先调用signal方法,然后在unlock解锁,如果在这两个操作中间该线程失去cpu,或者我人为的在siganl和unlock之间调用time.Sleep();在另一个等待线程中即使该等待线程被前者所发出的signal唤醒,但是唤醒的时候同时会去进行lock操作,但是前者的线程中由于失去了cpu,并没有调用unlock,那么这次唤醒不是应该失败了吗,即使前者有得到了cpu去执行了unlcok,但是signal操作具有及时性,等待线程不是应该继续等待下一个signal吗,感觉最后会变成死锁啊

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

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

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

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

    
     3
  • 打你
    2018-11-10
    在看了一遍,清楚了
    
     2
  • 打你
    2018-11-10
    lock.Lock()
    for mailbox == 1 {
     sendCond.Wait()
    }
    mailbox = 1
    lock.Unlock()
    recvCond.Signal()
    如果wait已经解锁lock.Lock()锁住的锁,后面lock.Unlock解锁是什么意思?不会panic。
    条件变量这2篇看起来前后理解不到
    展开
     2
     2
  • 樂文💤
    2019-08-03
    所以如果用的signal方法只通知一个go routine的话 条件变量的判断方法改成if应该是没问题的 但如果是多个go routine 同时被唤醒就可能导致多个go routine 在不满足唤醒状态的时候被唤醒而导致处理错误,这时候就必须用for来不断地进行检测可以这么理解么

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

     2
     1
  • RegExp
    2019-03-30
    条件变量的Wait方法在阻塞当前的 goroutine之前,会解锁它基于的互斥锁,那是不是显示调用lock.Lock()的锁也被解锁了呢?

    作者回复: 原理上是这样,但是实践中最好不要这样混用同一个锁。

    
     1
  • 甦
    2018-12-13
    源码问题问一下郝老师,cond wait方法里,多个协程走到c.L.Unlock()那一步不会出问题吗? 只有一个协程可以unlock成功,其他协程重复unlock不就panic了吗?
     1
     1
  • Alan
    2020-01-09
    demo61中的代码可以优化一下,去除发送者的defer语句和一行管道接收语句
    
    
  • 疯琴
    2019-12-10
    wait的原理讲解很清晰。

    请问老师两个问题:
    1. 收信人也修改了mailbox,为什么不用写锁?我试了一下,如果把收信人分成两个goroutine,一个循环2次另一个循环3次,有可能两个收信人先后连续收了信,这样最后一步是发信人发信。recvCond.Signal()只唤醒一个收信人,为什么会有两个收信人先后连续收信呢?我把recvCond改成用写锁就恢复正常了。用写锁是为了演示一下获取写锁的方法么?
    package main

    import (
        "log"
        "sync"
        "time"
    )

    func main() {
        var mailbox uint8
        var lock sync.RWMutex
        sendCond := sync.NewCond(&lock)
        recvCond := sync.NewCond(lock.RLocker())

        sign := make(chan struct{}, 3)
        max := 5

        // 发信
        go func(max int) {
            defer func() {
                sign <- struct{}{}
            }()

            for i := 1; i <= max; i++ {
                time.Sleep(time.Millisecond * 500)
                lock.Lock()
                for mailbox == 1 {
                    sendCond.Wait()
                }
                log.Printf("sender [%d]: the mail box is empty.", i)
                mailbox = 1
                log.Printf("sender [%d]: the letter has been send.", i)
                lock.Unlock()
                recvCond.Signal()
            }
        }(max)

        // 收信
        go func(max int) {
            defer func() {
                sign <- struct{}{}
            }()
            for j := 1; j <= max; j++ {
                time.Sleep(time.Millisecond * 500)
                lock.RLock()
                for mailbox == 0 {
                    recvCond.Wait()
                }
                log.Printf("receiver [%d]: the mail box is full.", j)
                mailbox = 0
                log.Printf("receiver [%d]: the letter has been received.", j)
                lock.RUnlock()
                sendCond.Signal()
            }
        }(2)

        // 收信
        go func(max int) {
            defer func() {
                sign <- struct{}{}
            }()
            for j := 1; j <= max; j++ {
                time.Sleep(time.Millisecond * 500)
                lock.RLock()
                for mailbox == 0 {
                    recvCond.Wait()
                }
                log.Printf("receiver [%d]: the mail box is full.", j)
                mailbox = 0
                log.Printf("receiver [%d]: the letter has been received.", j)
                lock.RUnlock()
                sendCond.Signal()
            }
        }(3)

        <-sign
        <-sign
        <-sign

        log.Println(mailbox)
    }
    2. “当通知被发送的时候,如果没有任何 goroutine 需要被唤醒,那么该通知就会立即失效。”这一点我没有理解。我让收信人启动更晚一点,也就是发信人执行Signal()的时候收信人还没有Wait(),但是依然正常执行了。
    展开

    作者回复: 先说第二点吧,真正的谁先谁后是在底层调度的。所以我们很难干涉。很可能你感觉是A先B后,到了实际调度的时候却是B先A后。另外,条件变量的行为规范就是如此。所以记住就好了。

    关于第一个问题,我又看了一下示例代码。在 demo61.go中确实是为了演示用法的。因为那里只有单发单收,所以不会有问题。而在demo62.go中就全是基于互斥锁了,那里是单发多收的。你可以看一下文章中每一段对应的是哪一个源码文件。

    
    
  • K_night
    2019-11-26
    send := func(id int, index int) {
            // defer func() {
            //     sign <- struct{}{}
            // }()
            lock.Lock()
            wakeupNum := 0
            log.Printf("wait before wakeupNum: [%d]", wakeupNum)
            for msgbox == 1 {
                fmt.Printf("wait before in for wakeupNum: [%d]", wakeupNum)
                // log.Printf("wait before in for wakeupNum: [%d]", wakeupNum)
                sendCond.Wait()
                // log.Printf("other Broadcast")
                wakeupNum = 1
                log.Printf("wait after wakeupNum: [%d]", wakeupNum)
            }
            log.Printf("sender: id:[%d] index:[%d] msgbox is empty", id, index)
            log.Printf("wakeupNum: [%d]", wakeupNum)
            sendNum = wakeupNum
            msgbox = 1
            log.Printf("sender: id:[%d] index:[%d] mail has been send", id, index)
            lock.Unlock()
            recvCond.Broadcast()
        }
    for循环里面的代码被优化了吗。怎么没有执行呢。望老师解惑感谢
    展开

    作者回复: 你这样给我代码我没法判断啊。你都改了哪些地方?

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

    作者回复: 我记得在书里也说过类似的内容:条件变量常用于对共享资源的状态变化的监控和响应(由此可以实现状态机),同时也比较适合帮助多个线程协作操作同一个共享资源(这里的关键词是协作)。

    条件变量有两种通知机制,这是互斥量没有的。互斥量只能保护共享资源(这里的关键词是保护),功能比较单一。所以条件变量在一些场景下会更高效。

    你自己也说出了一些使用两个互斥锁来做的弊端。这些弊端其实就已经比较闹心了。一个是用两个锁对性能的影响,一个是两个锁如果配合不当就会造成局部的死锁。这还是多方协作操作同一个共享资源中最简单的情况。

    再加上我前面说的那些,条件变量在此类场景下的优势已经非常明显了。注意它在Wait时还有原子级的操作呢。

    
    
  • 党
    2019-07-29
    看这函数的定义 找到了js的风格
    
    
  • 不记年
    2019-07-24
    唤醒用的Signal方法
    
    
  • 大中华区低端人口
    2019-06-18
    还是在用for的第一点情况上懵逼
    
    
  • 糊李糊涂
    2019-06-12
    看不懂,就结合例子再看一遍,就懂了
    
    
我们在线,来聊聊吧