Go语言核心36讲(Go语言实战与应用六)--学习笔记
28 | 条件变量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方法,并继续等待下次通知的到来。
这种情况是很有可能发生的,具体如下面所示。有多个 goroutine 在等待共享资源的同一种状态。比如
1、有多个 goroutine 在等待共享资源的同一种状态。比如,它们都在等mailbox变量的值不为0的时候再把它的值变为0,这就相当于有多个人在等着我向信箱里放置情报。虽然等待的 goroutine 有多个,但每次成功的 goroutine 却只可能有一个。别忘了,条件变量的Wait方法会在当前的 goroutine 醒来后先重新锁定那个互斥锁。在成功的 goroutine 最终解锁互斥锁之后,其他的 goroutine 会先后进入临界区,但它们会发现共享资源的状态依然不是它们想要的。这个时候,for循环就很有必要了。
2、共享资源可能有的状态不是两个,而是更多。比如,mailbox变量的可能值不只有0和1,还有2、3、4。这种情况下,由于状态在每次改变后的结果只可能有一个,所以,在设计合理的前提下,单一的结果一定不可能满足所有 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变量的类型,以及发送通知的方式。
package main
import (
"log"
"sync"
"time"
)
func main() {
// mailbox 代表信箱。
// 0代表信箱是空的,1代表信箱是满的。
var mailbox uint8
// lock 代表信箱上的锁。
var lock sync.Mutex
// sendCond 代表专用于发信的条件变量。
sendCond := sync.NewCond(&lock)
// recvCond 代表专用于收信的条件变量。
recvCond := sync.NewCond(&lock)
// send 代表用于发信的函数。
send := func(id, index int) {
lock.Lock()
for mailbox == 1 {
sendCond.Wait()
}
log.Printf("sender [%d-%d]: the mailbox is empty.",
id, index)
mailbox = 1
log.Printf("sender [%d-%d]: the letter has been sent.",
id, index)
lock.Unlock()
recvCond.Broadcast()
}
// recv 代表用于收信的函数。
recv := func(id, index int) {
lock.Lock()
for mailbox == 0 {
recvCond.Wait()
}
log.Printf("receiver [%d-%d]: the mailbox is full.",
id, index)
mailbox = 0
log.Printf("receiver [%d-%d]: the letter has been received.",
id, index)
lock.Unlock()
sendCond.Signal() // 确定只会有一个发信的goroutine。
}
// sign 用于传递演示完成的信号。
sign := make(chan struct{}, 3)
max := 6
go func(id, max int) { // 用于发信。
defer func() {
sign <- struct{}{}
}()
for i := 1; i <= max; i++ {
time.Sleep(time.Millisecond * 500)
send(id, i)
}
}(0, max)
go func(id, max int) { // 用于收信。
defer func() {
sign <- struct{}{}
}()
for j := 1; j <= max; j++ {
time.Sleep(time.Millisecond * 200)
recv(id, j)
}
}(1, max/2)
go func(id, max int) { // 用于收信。
defer func() {
sign <- struct{}{}
}()
for k := 1; k <= max; k++ {
time.Sleep(time.Millisecond * 200)
recv(id, k)
}
}(2, max/2)
<-sign
<-sign
<-sign
}
总结
我们今天主要讲了条件变量,它是基于互斥锁的一种同步工具。在 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是做什么用的?我们可以在使用条件变量的过程中改变这个字段的值吗?
笔记源码
https://github.com/MingsonZheng/go-core-demo
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。
Go语言核心36讲(Go语言实战与应用六)--学习笔记的更多相关文章
- Go语言核心36讲(Go语言实战与应用二)--学习笔记
24 | 测试的基本规则和流程(下) Go 语言是一门很重视程序测试的编程语言,所以在上一篇中,我与你再三强调了程序测试的重要性,同时,也介绍了关于go test命令的基本规则和主要流程的内容.今天我 ...
- Go语言核心36讲(Go语言基础知识三)--学习笔记
03 | 库源码文件 在我的定义中,库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 Go 语言规范的话). 这里的"其他代码" ...
- Go语言核心36讲(Go语言实战与应用一)--学习笔记
23 | 测试的基本规则和流程 (上) 在接下来的日子里,我将带你去学习在 Go 语言编程进阶的道路上,必须掌握的附加知识,比如:Go 程序测试.程序监测,以及 Go 语言标准库中各种常用代码包的正确 ...
- Go语言核心36讲(Go语言实战与应用三)--学习笔记
25 | 更多的测试手法 在本篇文章,我会继续为你讲解更多更高级的测试方法.这会涉及testing包中更多的 API.go test命令支持的,更多标记更加复杂的测试结果,以及测试覆盖度分析等等. 前 ...
- Go语言核心36讲(Go语言实战与应用四)--学习笔记
26 | sync.Mutex与sync.RWMutex 从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包.这会涉及这些代码包的标准用法.使用禁忌.背后原理以及周边的知识. ...
- Go语言核心36讲(Go语言实战与应用十二)--学习笔记
34 | 并发安全字典sync.Map (上) 我们今天再来讲一个并发安全的高级数据结构:sync.Map.众所周知,Go 语言自带的字典类型map并不是并发安全的. 前导知识:并发安全字典诞生史 换 ...
- Go语言核心36讲(Go语言实战与应用十四)--学习笔记
36 | unicode与字符编码 在开始今天的内容之前,我先来做一个简单的总结. Go 语言经典知识总结 在数据类型方面有: 基于底层数组的切片: 用来传递数据的通道: 作为一等类型的函数: 可实现 ...
- Go语言核心36讲(Go语言实战与应用十八)--学习笔记
40 | io包中的接口和工具 (上) 我们在前几篇文章中,主要讨论了strings.Builder.strings.Reader和bytes.Buffer这三个数据类型. 知识回顾 还记得吗?当时我 ...
- Go语言核心36讲(Go语言实战与应用二十二)--学习笔记
44 | 使用os包中的API (上) 我们今天要讲的是os代码包中的 API.这个代码包可以让我们拥有操控计算机操作系统的能力. 前导内容:os 包中的 API 这个代码包提供的都是平台不相关的 A ...
- Go语言核心36讲(Go语言实战与应用二十四)--学习笔记
46 | 访问网络服务 前导内容:socket 与 IPC 人们常常会使用 Go 语言去编写网络程序(当然了,这方面也是 Go 语言最为擅长的事情).说到网络编程,我们就不得不提及 socket. s ...
随机推荐
- 【Go】Golang实现gRPC的Proxy的原理
背景 gRPC是Google开始的一个RPC服务框架, 是英文全名为Google Remote Procedure Call的简称. 广泛的应用在有RPC场景的业务系统中,一些架构中将gRPC请求都经 ...
- CF536D Tavas in Kansas(博弈论+dp)
貌似洛谷的题面是没有翻译的 QWQ 大致题面是这个样子,但是可能根据题目本身有不同的地方 完全懵逼的一个题(果然博弈论就是不一样) 首先,我们考虑把题目转化成一个可做的模型. 我们分别从\(s\)和\ ...
- vue基础-动态样式&表单绑定&vue响应式原理
动态样式 作用:使用声明式变量来控制class和style的值 语法: :class/:style 注意:尽可能不要把动态class和静态class一起使用,原因动态class起作用的时间会比较晚,需 ...
- Go 里的超时控制
前言 日常开发中我们大概率会遇到超时控制的场景,比如一个批量耗时任务.网络请求等:一个良好的超时控制可以有效的避免一些问题(比如 goroutine 泄露.资源不释放等). Timer 在 go 中实 ...
- FastAPI 学习之路(三十三)操作数据库
通过创建pydantic模型进行验证提交数据 from pydantic import BaseModel class UserBase(BaseModel): email: str class Us ...
- 生产环境部署springcloud微服务启动慢的问题排查
今天带来一个真实案例,虽然不是什么故障,但是希望对大家有所帮助. 一.问题现象: 生产环境部署springcloud应用,服务部署之后,有时候需要10几分钟才能启动成功,在开发测试环境则没有这个问题. ...
- camera isp(Image Signal Processor)
1. 目标[52RD.com] 手机摄像头模组用ISP功能模块的市场走向及研发方向.为能够正确认识手机摄像模组行业提供技术及市场依据.[52RD.com] 2. ISP在模组上的应用原理[52RD.c ...
- 国产Linux服务器-Jexus的初步使用
题记:年末研究了一些关于Net跨平台的东西,没错,就是Jexus,就是Windows下面的IIS. 官网:https://www.jexus.org/ 先看看官网的解释再说其他的问题,Jexus就是L ...
- linux exit 和 _exit的区别
今天仔细看了一下exit和_exit这两个函数的区别,实际上exit也是调用了_exit退出函数的,只不过在调用_exit之前,exit还进行了一些多余的工作,也正是因为这样,相比起来exit就没有那 ...
- triangle leetcode C++
Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent n ...