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

27 | 条件变量sync.Cond (上)

sendCond.Signal()
lock.RUnlock()
mailbox = 0
for mailbox == 0 { recvCond.Wait() }
lock.RLock()
recvCond.Signal()
lock.Unlock()
mailbox = 1
for mailbox == 1 { sendCond.Wait() }
lock.Lock()
recvCond
sendCond
lock
mailbox
sync.Cond类型的值传递
*sync.Cond类型的值传递
适时获取情报并通知
适时放置情报并通知
创建变量
广播通知(broadcast)
单发通知(signal)
等待通知(wait)
方法基于互斥锁
初始化离不开互斥锁
例子:两人访问信箱
通知被互斥锁阻塞的线程
协调线程访问共享资源
互斥锁的支撑
同步工具
互斥锁
思考题
代码实现
使用条件变量
条件变量

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

在上篇文章中,我们主要说的是互斥锁,今天我和你来聊一聊条件变量(conditional variable)。

前导内容:条件变量与互斥锁

我们常常会把条件变量这个同步工具拿来与互斥锁一起讨论。实际上,条件变量是基于互斥锁的,它必须有互斥锁的支撑才能发挥作用。
条件变量并不是被用来保护临界区和共享资源的,它是用于协调想要访问共享资源的那些线程的。当共享资源的状态发生变化时,它可以被用来通知被互斥锁阻塞的线程。
比如说,我们两个人在共同执行一项秘密任务,这需要在不直接联系和见面的前提下进行。我需要向一个信箱里放置情报,你需要从这个信箱中获取情报。这个信箱就相当于一个共享资源,而我们就分别是进行写操作的线程和进行读操作的线程。
如果我在放置的时候发现信箱里还有未被取走的情报,那就不再放置,而先返回。另一方面,如果你在获取的时候发现信箱里没有情报,那也只能先回去了。这就相当于写的线程或读的线程阻塞的情况。
虽然我们俩都有信箱的钥匙,但是同一时刻只能有一个人插入钥匙并打开信箱,这就是锁的作用了。更何况咱们俩是不能直接见面的,所以这个信箱本身就可以被视为一个临界区。
尽管没有协调好,咱们俩仍然要想方设法的完成任务啊。所以,如果信箱里有情报,而你却迟迟未取走,那我就需要每过一段时间带着新情报去检查一次,若发现信箱空了,我就需要及时地把新情报放到里面。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

条件变量是基于互斥锁的同步工具,用于协调多个线程对共享资源的访问。它通过等待通知、单发通知和广播通知的方式,实现线程间的协作。条件变量的使用离不开互斥锁的支持,需要在互斥锁的保护下进行等待通知操作,而在单发通知或广播通知时则需要在互斥锁解锁之后进行。通过条件变量,线程不再需要循环检查共享资源的状态,而是等待通知,从而提高了效率。条件变量的典型应用场景可以通过一个秘密任务的例子来说明,其中条件变量就相当于两个戴不同颜色帽子的小孩儿,用于通知共享资源状态的变化。因此,条件变量在提高程序效率方面具有重要作用。 文章通过代码实现了条件变量的使用,包括创建变量、初始化条件变量、以及使用条件变量进行线程协作的具体流程。作者通过实例详细解释了条件变量的使用方法,以及条件变量与互斥锁的配合使用。最后,作者总结了条件变量的基本特点和使用规则,并提出了思考题,引发读者对条件变量的深入思考。 总的来说,本文通过实例和代码详细介绍了条件变量的使用方法和原理,适合读者快速了解条件变量的概况,以及在实际编程中如何使用条件变量进行线程协作。

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

全部留言(42)

  • 最新
  • 精选
  • hello peter
    老师, 感觉这个送信的例子似乎用chanel实现更简单.在网上也查了一些例子, 发现都可以用chanel替代. 那使用sync.Cond 的优势是什么呢, 或者有哪些独特的使用场景?

    作者回复: 优势是并发流程上的协同,chan的主要任务是传递数据。另外cond是更低层次的工具,效率更高一些,但是肯定没有chan方便。

    2018-10-26
    2
    38
  • 属雨
    个人理解,不确定对不对,请老师评判一下: 因为Go语言传递对象时,使用的是浅拷贝的值传递,所以,当传递一个Cond对象时复制了这个Cond对象,但是低层保存的L(Locker类型),noCopy(noCopy类型),notify(notifyList类型),checker(copyChecker)对象的指针没变,因此,*sync.Cond和sync.Cond都可以传递。

    作者回复: 基本正确。Locker是接口,是引用类型,nocopy是结构体,所以直接拷贝值的话,底层锁还是用的同一个,使用上容易出问题。

    2018-10-12
    33
  • 传说中的成大大
    这几天一直对条件变量的理解比较模糊,但是我想既然要学就学好 于是又去翻了Unix环境高级编程 总算把它跟互斥锁区分开了 互斥锁 是对一个共享区域进行加锁 所有线程都是一种竞争的状态去访问 而条件变量 主要是通过条件状态来判断,实际上他还是会阻塞 只不过不会像互斥锁一样去参与竞争,而是在哪里等待条件变量的状态发生改变过后的通知 再被唤醒

    作者回复: 👍

    2020-04-09
    19
  • Geek_a8be59
    var mailbox uint8 var lock sync.RWMutex sendCond := sync.NewCond(&lock) recvCond := sync.NewCond(&lock) 为什么不能向上面那样都用同一个互斥量,非要两个不同呢?老师,能讲一下区别么

    作者回复: 如果都用同一个互斥量的话,操作双方就无法独立行事,这就是完全串行的操作了,效率上会大打折扣。 进一步说,本来就是一个发一个收,理应一个用写锁一个用读锁,这样效率高,之后扩展起来也方便。因为读之间不用互斥。

    2019-08-12
    3
    15
  • 郭星
    在for 循环中使用 wait,在我的测试中,当条件变量处于wait状态时,如果没有唤醒,当前协程会一直阻塞等待在wait这行代码,因此使用for 和 使用if 实际最终结果是相同的,为什么要使用for呢? package lesson27 import ( "sync" "testing" "time" ) // 利用条件变量实现协调多协程发取信件操作 func TestCond(t *testing.T) { var wg sync.WaitGroup var mu sync.RWMutex // 信箱 mail := false // 两个条件变量 // 发送信条件变量 sendCond := sync.NewCond(&mu) // 接收信条件变量, 对于接收实际是只读操作,因此只需要使用读锁就可以 receiveCond := sync.NewCond(mu.RLocker()) // 最大发送接收次数 max := 5 wg.Add(2) // 发送人协程 go func(i int) { for ; i > 0; i-- { time.Sleep(time.Second * 3) mu.Lock() // 如果信箱不为空,则需要等待 //for mail { if mail { // 发送者等待 t.Log("sendCond准备进入等待队列") sendCond.Wait() t.Log("sendCond进入等待队列") } mail = true t.Log("发送信件成功") mu.Unlock() // 通知发送者 receiveCond.Signal() t.Log("唤醒receiveCond") } wg.Done() }(max) go func(i int) { for ; i > 0; i-- { mu.RLock() //for !mail { if !mail { //接收者等待 t.Log("receiveCond准备进入等待队列") receiveCond.Wait() // 如果没有被唤醒会一直阻塞在此 t.Log("receiveCond进入等待队列") } mail = false t.Log("获取信件成功") mu.RUnlock() // 通知接收者 sendCond.Signal() t.Log("唤醒sendCond") } wg.Done() }(max) wg.Wait() }

    作者回复: 我在文章里说了啊,有可能会碰到“假唤醒”的情况。而且,如果存在“有多个wait但只需唤醒一个”的情况,也需要用for语句。在for语句里,唤醒后可以再次检查状态,如果状态符合就开始后续工作,如果不符合就再次wait。用if语句就办不到。

    2020-09-02
    5
  • lesserror
    郝林老师,demo61.go 中的 两个go function(收信 和 发信),是怎么保证先 发信 后收信的呢? 不是说 go function 函数 的执行 是 随机的么? 我打印了很多遍,发现 都是执行的 发信 操作,然后是 收信 操作。

    作者回复: 正因为有了条件变量才会有这样的同步状态啊。条件变量就相当于信号弹的发射器。 双方各有一个信号弹发射器(就像示例程序中的 sendCond 和 recvCond)并互相发送信号,这就是 demo61 所做的。 这个程序你也可以这样理解: A 和 B 在下象棋。步骤如下: A 走了一步棋,并发送信号示意让 B 走一步; B 收到信号走了一步棋,然后反过来向 A 发送信号示意“我走了一步,该你了”; A 再走一步棋,并再次发射信号示意让 B 再走一步; B 再走一步后,再发射信号告诉 A; 如此循环往复。 再做一个类比: 单个条件变量用于“教官”指挥“士兵”,“教官”只能有一个,而“士兵”可以有多个(demo61 中只有一个士兵)。在 demo61 中,我们又多加了一个用于“反馈”的信号弹发射器。这样一来,“士兵”按照指挥行进一步之后,就可以及时告诉指挥官并请求下一步的行动了。

    2021-08-15
    3
  • 啦啦啦
    想请问下老师,两个goroutine都使用了同一把锁,26讲(Mutex)里不是说明,尽量使用:是让每一个互斥锁都只保护一个临界区或一组相关临界区。有点搞不明白,望老师指点 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 mailbox is empty.", i) mailbox = 1 log.Printf("sender [%d]: the letter has been sent.", 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 mailbox is full.", j) mailbox = 0 log.Printf("receiver [%d]: the letter has been received.", j) lock.RUnlock() sendCond.Signal() } }(max)

    作者回复: “一组相关临界区”可以是操作同一个共享资源的多段代码,也可以是在逻辑上有强相关关系的多段代码。在这里,这两段代码都是针对 mailbox 变量的,属于前者。只不过其中还夹杂了 不同条件变量 的 Wait 操作。 我总体再说一下吧,我们可以从两个维度来理解互斥锁的保护作用: 1. 多个goroutine并发执行同一段代码(如一个函数或方法),且这段代码访问或修改了共享的状态或资源。这时候,我们需要使用互斥锁保护这段代码。这相当于保护了那个共享状态或资源。这里的“这段代码”就是一个临界区。 2. (如本例)多个goroutine中的代码(确切地说,是go函数)直接访问或修改了共享的状态或资源。这时候,我们往往就需要使用同一个互斥锁同时去保护那几段相关的代码。这里的几段代码各自成为独立的临界区,但它们是相关的。这就是我说的“一组相关的临界区”。 以上这两个维度说的就是互斥锁的常用场景。一个是“单段代码的并发执行”,一个是“多段代码的各自执行(也是并发执行)”。关键是,不管是单段代码还是多段代码,它们都分别访问了同一个共享的东西。

    2022-06-07
    2
  • 会玩code
    老师,不懂这里的recvCond为什么可以用读锁呢?这里也是有对资源做操作的呀(将mailbox置为0),用读锁不会有问题吗?

    作者回复: 在只有一个“取信者”的情况下,这里使用读锁是没问题的。但如果有多个“取信者”就需要用写锁啦。你可以看一看下一篇文章对应的demo文件(demo62.go),其中就有多个“取信者”的演示。

    2020-05-15
    2
    2
  • lofaith
    老师,读写锁之间不是互斥的吗,我理解应该在加上读锁的时候,写锁就会阻塞在lock这里,不会走到 sendCond.Wait() 这里啊。虽然能明白条件变量的作用了,但还是不清楚它的使用场景,老师能说一下使用场景吗

    作者回复: 读写锁中的读-写、写-写是互斥的,但是读-读不互斥。 ``` lock.Lock() for mailbox == 1 { sendCond.Wait() // 至于这里都做了什么,你看到下集应该就明白了 } mailbox = 1 lock.Unlock() recvCond.Signal() ``` 说到场景,条件变量最简单的应用场景就是生产-消费协作。这篇文章中的例子只不过是“生产-消费协作”的双向叠加版。 因此,我们在分别用多个goroutine针对同一个共享资源进行读写的时候就可以运用条件变量,尤其是“单写多读”或“单读多写”的时候。发信号的一方作为“单”的一方,收信号的一方作为“多”的一方(或者“单”的另一方)。 这里的“单”是指,只有一个goroutine在进行读或写的操作,“多”自然是指有多个goroutine在进行相应的操作。 当然了,我们也可以把它适用于更复杂的场景,也就是存在多组“单写N读”或“单读N写”的情况。不过,每一组通常会分别对应不同的共享资源。 多组针对同一个共享资源的情况也是有的。本文的例子就是两组“单写单读”的情况,而且针对的是同一个共享资源,并互为“对立”信号的收发方(即双向叠加版)。 最后请记住,条件变量的重点在于多goroutine间的紧密协作,而非简单的互斥。

    2021-11-15
    1
  • ...
    老师 wait会释放锁吗

    作者回复: 每次执行结束前都会释放,要不其他goroutine没法进入锁保护的临界区。

    2019-02-20
    3
    1
收起评论
显示
设置
留言
42
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部