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

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

在了解了条件变量的使用方式之后,你可能会有这么几个疑问。

  1. 为什么先要锁定条件变量基于的互斥锁,才能调用它的Wait方法?
  2. 为什么要用for语句来包裹调用其Wait方法的表达式,用if语句不行吗?

这些问题我在面试的时候也经常问。你需要对这个Wait方法的内部机制有所了解才能回答上来。

条件变量的Wait方法主要做了四件事。

  1. 把调用它的goroutine(也就是当前的goroutine)加入到当前条件变量的通知队列中。
  2. 解锁当前的条件变量基于的那个互斥锁。
  3. 让当前的goroutine处于等待状态,等到通知到来时再决定是否唤醒它。此时,这个goroutine就会阻塞在调用这个Wait方法的那行代码上。
  4. 如果通知到来并且决定唤醒这个goroutine,那么就在唤醒它之后重新锁定当前条件变量基于的互斥锁。自此之后,当前的goroutine就会继续执行后面的代码了。

你现在知道我刚刚说的第一个疑问的答案了吗?

因为条件变量的Wait方法在阻塞当前的goroutine之前,会解锁它基于的互斥锁,所以在调用该Wait方法之前,我们必须先锁定那个互斥锁,否则在调用这个Wait方法时,就会引发一个不可恢复的panic。

为什么条件变量的Wait方法要这么做呢?你可以想象一下,如果Wait方法在互斥锁已经锁定的情况下,阻塞了当前的goroutine,那么又由谁来解锁呢?别的goroutine吗?

先不说这违背了互斥锁的重要使用原则,即:成对的锁定和解锁,就算别的goroutine可以来解锁,那万一解锁重复了怎么办?由此引发的panic可是无法恢复的。

如果当前的goroutine无法解锁,别的goroutine也都不来解锁,那么又由谁来进入临界区,并改变共享资源的状态呢?只要共享资源的状态不变,即使当前的goroutine因收到通知而被唤醒,也依然会再次执行这个Wait方法,并再次被阻塞。

所以说,如果条件变量的Wait方法不先解锁互斥锁的话,那么就只会造成两种后果:不是当前的程序因panic而崩溃,就是相关的goroutine全面阻塞。

再解释第二个疑问。很显然,if语句只会对共享资源的状态检查一次,而for语句却可以做多次检查,直到这个状态改变为止。那为什么要做多次检查呢?

这主要是为了保险起见。如果一个goroutine因收到通知而被唤醒,但却发现共享资源的状态,依然不符合它的要求,那么就应该再次调用条件变量的Wait方法,并继续等待下次通知的到来。

这种情况是很有可能发生的,具体如下面所示。

  1. 有多个goroutine在等待共享资源的同一种状态。比如,它们都在等mailbox变量的值不为0的时候再把它的值变为0,这就相当于有多个人在等着我向信箱里放置情报。虽然等待的goroutine有多个,但每次成功的goroutine却只可能有一个。别忘了,条件变量的Wait方法会在当前的goroutine醒来后先重新锁定那个互斥锁。在成功的goroutine最终解锁互斥锁之后,其他的goroutine会先后进入临界区,但它们会发现共享资源的状态依然不是它们想要的。这个时候,for循环就很有必要了。

  2. 共享资源可能有的状态不是两个,而是更多。比如,mailbox变量的可能值不只有01,还有234。这种情况下,由于状态在每次改变后的结果只可能有一个,所以,在设计合理的前提下,单一的结果一定不可能满足所有goroutine的条件。那些未被满足的goroutine显然还需要继续等待和检查。

  3. 有一种可能,共享资源的状态只有两个,并且每种状态都只有一个goroutine在关注,就像我们在主问题当中实现的那个例子那样。不过,即使是这样,使用for语句仍然是有必要的。原因是,在一些多CPU核心的计算机系统中,即使没有收到条件变量的通知,调用其Wait方法的goroutine也是有可能被唤醒的。这是由计算机硬件层面决定的,即使是操作系统(比如Linux)本身提供的条件变量也会如此。

综上所述,在包裹条件变量的Wait方法的时候,我们总是应该使用for语句。

好了,到这里,关于条件变量的Wait方法,我想你知道的应该已经足够多了。

问题 2:条件变量的Signal方法和Broadcast方法有哪些异同?

条件变量的Signal方法和Broadcast方法都是被用来发送通知的,不同的是,前者的通知只会唤醒一个因此而等待的goroutine,而后者的通知却会唤醒所有为此等待的goroutine。

条件变量的Wait方法总会把当前的goroutine添加到通知队列的队尾,而它的Signal方法总会从通知队列的队首开始,查找可被唤醒的goroutine。所以,因Signal方法的通知,而被唤醒的goroutine一般都是最早等待的那一个。

这两个方法的行为决定了它们的适用场景。如果你确定只有一个goroutine在等待通知,或者只需唤醒任意一个goroutine就可以满足要求,那么使用条件变量的Signal方法就好了。

否则,使用Broadcast方法总没错,只要你设置好各个goroutine所期望的共享资源状态就可以了。

此外,再次强调一下,与Wait方法不同,条件变量的Signal方法和Broadcast方法并不需要在互斥锁的保护下执行。恰恰相反,我们最好在解锁条件变量基于的那个互斥锁之后,再去调用它的这两个方法。这更有利于程序的运行效率。

最后,请注意,条件变量的通知具有即时性。也就是说,如果发送通知的时候没有goroutine为此等待,那么该通知就会被直接丢弃。在这之后才开始等待的goroutine只可能被后面的通知唤醒。

你可以打开demo62.go文件,并仔细观察它与demo61.go的不同。尤其是lock变量的类型,以及发送通知的方式。

总结

我们今天主要讲了条件变量,它是基于互斥锁的一种同步工具。在Go语言中,我们需要用sync.NewCond函数来初始化一个sync.Cond类型的条件变量。

sync.NewCond函数需要一个sync.Locker类型的参数值。

*sync.Mutex类型的值以及*sync.RWMutex类型的值都可以满足这个要求。另外,后者的RLocker方法可以返回这个值中的读锁,也同样可以作为sync.NewCond函数的参数值,如此就可以生成与读写锁中的读锁对应的条件变量了。

条件变量的Wait方法需要在它基于的互斥锁保护下执行,否则就会引发不可恢复的panic。此外,我们最好使用for语句来检查共享资源的状态,并包裹对条件变量的Wait方法的调用。

不要用if语句,因为它不能重复地执行“检查状态-等待通知-被唤醒”的这个流程。重复执行这个流程的原因是,一个“因为等待通知,而被阻塞”的goroutine,可能会在共享资源的状态不满足其要求的情况下被唤醒。

条件变量的Signal方法只会唤醒一个因等待通知而被阻塞的goroutine,而它的Broadcast方法却可以唤醒所有为此而等待的goroutine。后者比前者的适应场景要多得多。

这两个方法并不需要受到互斥锁的保护,我们也最好不要在解锁互斥锁之前调用它们。还有,条件变量的通知具有即时性。当通知被发送的时候,如果没有任何goroutine需要被唤醒,那么该通知就会立即失效。

思考题

sync.Cond类型中的公开字段L是做什么用的?我们可以在使用条件变量的过程中改变这个字段的值吗?

戳此查看Go语言专栏文章配套详细代码。

Go语言核心36讲30的更多相关文章

  1. Go语言核心36讲(导读)--学习笔记

    目录 开篇词 | 跟着学,你也能成为Go语言高手 导读 | 写给0基础入门的Go语言学习者 导读 | 学习专栏的正确姿势 开篇词 | 跟着学,你也能成为Go语言高手 Go 语言是由 Google 出品 ...

  2. Go语言核心36讲(Go语言进阶技术八)--学习笔记

    14 | 接口类型的合理运用 前导内容:正确使用接口的基础知识 在 Go 语言的语境中,当我们在谈论"接口"的时候,一定指的是接口类型.因为接口类型与其他数据类型不同,它是没法被实 ...

  3. Go语言核心36讲(Go语言进阶技术十六)--学习笔记

    22 | panic函数.recover函数以及defer语句(下) 我在前一篇文章提到过这样一个说法,panic 之中可以包含一个值,用于简要解释引发此 panic 的原因. 如果一个 panic ...

  4. Go语言核心36讲(Go语言实战与应用一)--学习笔记

    23 | 测试的基本规则和流程 (上) 在接下来的日子里,我将带你去学习在 Go 语言编程进阶的道路上,必须掌握的附加知识,比如:Go 程序测试.程序监测,以及 Go 语言标准库中各种常用代码包的正确 ...

  5. Go语言核心36讲(Go语言实战与应用三)--学习笔记

    25 | 更多的测试手法 在本篇文章,我会继续为你讲解更多更高级的测试方法.这会涉及testing包中更多的 API.go test命令支持的,更多标记更加复杂的测试结果,以及测试覆盖度分析等等. 前 ...

  6. Go语言核心36讲(Go语言实战与应用四)--学习笔记

    26 | sync.Mutex与sync.RWMutex 从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包.这会涉及这些代码包的标准用法.使用禁忌.背后原理以及周边的知识. ...

  7. Go语言核心36讲(Go语言实战与应用八)--学习笔记

    30 | 原子操作(下) 我们接着上一篇文章的内容继续聊,上一篇我们提到了,sync/atomic包中的函数可以做的原子操作有:加法(add).比较并交换(compare and swap,简称 CA ...

  8. Go语言核心36讲(Go语言实战与应用十四)--学习笔记

    36 | unicode与字符编码 在开始今天的内容之前,我先来做一个简单的总结. Go 语言经典知识总结 在数据类型方面有: 基于底层数组的切片: 用来传递数据的通道: 作为一等类型的函数: 可实现 ...

  9. Go语言核心36讲(Go语言实战与应用十八)--学习笔记

    40 | io包中的接口和工具 (上) 我们在前几篇文章中,主要讨论了strings.Builder.strings.Reader和bytes.Buffer这三个数据类型. 知识回顾 还记得吗?当时我 ...

  10. Go语言核心36讲(Go语言实战与应用二十二)--学习笔记

    44 | 使用os包中的API (上) 我们今天要讲的是os代码包中的 API.这个代码包可以让我们拥有操控计算机操作系统的能力. 前导内容:os 包中的 API 这个代码包提供的都是平台不相关的 A ...

随机推荐

  1. python_跨文件二维全局变量传参

    因为业务需要,我需要创建一个可以在多模块公用全局变量,根据https://www.jianshu.com/p/6cee728f3490的代码,因为他提供的只能生成 { k: v, kk: v ... ...

  2. Vmware虚拟主机访问外网设置

    本手册使用10.4.7.0/24网段 重点在于虚拟主机的网关和宿主机上的Vmnet8的IP和虚拟网络编辑器的NET网关保持一致 1.设置宿主机网络适配器 选择允许Vmware网络共享 配置VMnet8 ...

  3. Linux命令之find、grep、echo、tar、whoami、uname

    1. whoami--查看当前登录的用户名 book@100ask:~/linux$ whoami book 2. echo--打印命令,配合'>'或者'>>'使用 echo 打印信 ...

  4. 输入法词库解析(三)紫光拼音词库.uwl

    详细代码:https://github.com/cxcn/dtool 前言 .uwl 是紫光拼音输入法(现在叫华宇拼音输入法)使用的词库. 解析 紫光的词库有点复杂,拼音用的索引,但是拼音表没有写在词 ...

  5. 7.云原生之Docker容器Dockerfile镜像构建浅析与实践

    转载自:https://www.bilibili.com/read/cv15220707/?from=readlist Dockerfile 镜像构建浅析与实践 描述:Dockerfile是一个文本格 ...

  6. CentOS系统一键部署jdk,maven,tomcat,mysql

    #!/bin/bash ####使用方法############### # chmod a+x JdTomK-Auto.sh # source JdTomK-Auto.sh ############# ...

  7. Logstash:如何处理 Logstash pipeline 错误信息

    转载自:https://elasticstack.blog.csdn.net/article/details/114290663 在我们使用 Logstash 的时候经常会出现一些错误.比如当我们使用 ...

  8. PostgreSQL 语法

    进入命令行工具,我们可以使用 \help 来查看各个命令的语法 : postgres-# \help <command_name> 例如,我们查看下 select 语句的语法: postg ...

  9. PHP全栈开发(四): HTML 学习(2. div 布局)

    无序列表,有序列表,自定义列表 无序列表是ul表示,每个元素用li表示 有序列表是ol表示,每个元素用li表示 <ul> <li>首页</li><li> ...

  10. VideoPipe可视化视频结构化框架开源了!

    完成多路视频并行接入.解码.多级推理.结构化数据分析.上报.编码推流等过程,插件式/pipe式编程风格,功能上类似英伟达的deepstream和华为的mxvision,但底层核心不依赖复杂难懂的gst ...