Go语言核心36讲29
在上篇文章中,我们主要说的是互斥锁,今天我和你来聊一聊条件变量(conditional variable)。
前导内容:条件变量与互斥锁
我们常常会把条件变量这个同步工具拿来与互斥锁一起讨论。实际上,条件变量是基于互斥锁的,它必须有互斥锁的支撑才能发挥作用。
条件变量并不是被用来保护临界区和共享资源的,它是用于协调想要访问共享资源的那些线程的。当共享资源的状态发生变化时,它可以被用来通知被互斥锁阻塞的线程。
比如说,我们两个人在共同执行一项秘密任务,这需要在不直接联系和见面的前提下进行。我需要向一个信箱里放置情报,你需要从这个信箱中获取情报。这个信箱就相当于一个共享资源,而我们就分别是进行写操作的线程和进行读操作的线程。
如果我在放置的时候发现信箱里还有未被取走的情报,那就不再放置,而先返回。另一方面,如果你在获取的时候发现信箱里没有情报,那也只能先回去了。这就相当于写的线程或读的线程阻塞的情况。
虽然我们俩都有信箱的钥匙,但是同一时刻只能有一个人插入钥匙并打开信箱,这就是锁的作用了。更何况咱们俩是不能直接见面的,所以这个信箱本身就可以被视为一个临界区。
尽管没有协调好,咱们俩仍然要想方设法的完成任务啊。所以,如果信箱里有情报,而你却迟迟未取走,那我就需要每过一段时间带着新情报去检查一次,若发现信箱空了,我就需要及时地把新情报放到里面。
另一方面,如果信箱里一直没有情报,那你也要每过一段时间去打开看看,一旦有了情报就及时地取走。这么做是可以的,但就是太危险了,很容易被敌人发现。
后来,我们又想了一个计策,各自雇佣了一个不起眼的小孩儿。如果早上七点有一个戴红色帽子的小孩儿从你家楼下路过,那么就意味着信箱里有了新情报。另一边,如果上午九点有一个戴蓝色帽子的小孩儿从我家楼下路过,那就说明你已经从信箱中取走了情报。
这样一来,咱们执行任务的隐蔽性高多了,并且效率的提升非常显著。这两个戴不同颜色帽子的小孩儿就相当于条件变量,在共享资源的状态产生变化的时候,起到了通知的作用。
当然了,我们是在用Go语言编写程序,而不是在执行什么秘密任务。因此,条件变量在这里的最大优势就是在效率方面的提升。当共享资源的状态不满足条件的时候,想操作它的线程再也不用循环往复地做检查了,只要等待通知就好了。
说到这里,想考考你知道怎么使用条件变量吗?所以,我们今天的问题就是:条件变量怎样与互斥锁配合使用?
这道题的典型回答是:条件变量的初始化离不开互斥锁,并且它的方法有的也是基于互斥锁的。
条件变量提供的方法有三个:等待通知(wait)、单发通知(signal)和广播通知(broadcast)。
我们在利用条件变量等待通知的时候,需要在它基于的那个互斥锁保护下进行。而在进行单发通知或广播通知的时候,却是恰恰相反的,也就是说,需要在对应的互斥锁解锁之后再做这两种操作。
问题解析
这个问题看起来很简单,但其实可以基于它,延伸出很多其他的问题。比如,每个方法的使用时机是什么?又比如,每个方法执行的内部流程是怎样的?
下面,我们一边用代码实现前面那个例子,一边讨论条件变量的使用。
首先,我们先来创建如下几个变量。
var mailbox uint8
var lock sync.RWMutex
sendCond := sync.NewCond(&lock)
recvCond := sync.NewCond(lock.RLocker())
变量mailbox
代表信箱,是uint8
类型的。 若它的值为0
则表示信箱中没有情报,而当它的值为1
时则说明信箱中有情报。lock
是一个类型为sync.RWMutex
的变量,是一个读写锁,也可以被视为信箱上的那把锁。
另外,基于这把锁,我还创建了两个代表条件变量的变量,名字分别叫sendCond
和recvCond
。 它们都是*sync.Cond
类型的,同时也都是由sync.NewCond
函数来初始化的。
与sync.Mutex
类型和sync.RWMutex
类型不同,sync.Cond
类型并不是开箱即用的。我们只能利用sync.NewCond
函数创建它的指针值。这个函数需要一个sync.Locker
类型的参数值。
还记得吗?我在前面说过,条件变量是基于互斥锁的,它必须有互斥锁的支撑才能够起作用。因此,这里的参数值是不可或缺的,它会参与到条件变量的方法实现当中。
sync.Locker
其实是一个接口,在它的声明中只包含了两个方法定义,即:Lock()
和Unlock()
。sync.Mutex
类型和sync.RWMutex
类型都拥有Lock
方法和Unlock
方法,只不过它们都是指针方法。因此,这两个类型的指针类型才是sync.Locker
接口的实现类型。
我在为sendCond
变量做初始化的时候,把基于lock
变量的指针值传给了sync.NewCond
函数。
原因是,lock
变量的Lock
方法和Unlock
方法分别用于对其中写锁的锁定和解锁,它们与sendCond
变量的含义是对应的。sendCond
是专门为放置情报而准备的条件变量,向信箱里放置情报,可以被视为对共享资源的写操作。
相应的,recvCond
变量代表的是专门为获取情报而准备的条件变量。 虽然获取情报也会涉及对信箱状态的改变,但是好在做这件事的人只会有你一个,而且我们也需要借此了解一下,条件变量与读写锁中的读锁的联用方式。所以,在这里,我们暂且把获取情报看做是对共享资源的读操作。
因此,为了初始化recvCond
这个条件变量,我们需要的是lock
变量中的读锁,并且还需要是sync.Locker
类型的。
可是,lock
变量中用于对读锁进行锁定和解锁的方法却是RLock
和RUnlock
,它们与sync.Locker
接口中定义的方法并不匹配。
好在sync.RWMutex
类型的RLocker
方法可以实现这一需求。我们只要在调用sync.NewCond
函数时,传入调用表达式lock.RLocker()
的结果值,就可以使该函数返回符合要求的条件变量了。
为什么说通过lock.RLocker()
得来的值就是lock
变量中的读锁呢?实际上,这个值所拥有的Lock
方法和Unlock
方法,在其内部会分别调用lock
变量的RLock
方法和RUnlock
方法。也就是说,前两个方法仅仅是后两个方法的代理而已。
好了,我们现在有四个变量。一个是代表信箱的mailbox
,一个是代表信箱上的锁的lock
。还有两个是,代表了蓝帽子小孩儿的sendCond
,以及代表了红帽子小孩儿的recvCond
。
(互斥锁与条件变量)
我,现在是一个goroutine(携带的go
函数),想要适时地向信箱里放置情报并通知你,应该怎么做呢?
lock.Lock()
for mailbox == 1 {
sendCond.Wait()
}
mailbox = 1
lock.Unlock()
recvCond.Signal()
我肯定需要先调用lock
变量的Lock
方法。注意,这个Lock
方法在这里意味的是:持有信箱上的锁,并且有打开信箱的权利,而不是锁上这个锁。
然后,我要检查mailbox
变量的值是否等于1
,也就是说,要看看信箱里是不是还存有情报。如果还有情报,那么我就回家去等蓝帽子小孩儿了。
这就是那条for
语句以及其中的调用表达式sendCond.Wait()
所表示的含义了。你可能会问,为什么这里是for
语句而不是if
语句呢?我在后面会对此进行解释的。
我们再往后看,如果信箱里没有情报,那么我就把新情报放进去,关上信箱、锁上锁,然后离开。用代码表达出来就是mailbox = 1
和lock.Unlock()
。
离开之后我还要做一件事,那就是让红帽子小孩儿准时去你家楼下路过。也就是说,我会及时地通知你“信箱里已经有新情报了”,我们调用recvCond
的Signal
方法就可以实现这一步骤。
另一方面,你现在是另一个goroutine,想要适时地从信箱中获取情报,然后通知我。
lock.RLock()
for mailbox == 0 {
recvCond.Wait()
}
mailbox = 0
lock.RUnlock()
sendCond.Signal()
你跟我做的事情在流程上其实基本一致,只不过每一步操作的对象是不同的。你需要调用的是lock
变量的RLock
方法。因为你要进行的是读操作,并且会使用recvCond
变量作为辅助。recvCond
与lock
变量的读锁是对应的。
在打开信箱后,你要关注的是信箱里是不是没有情报,也就是检查mailbox
变量的值是否等于0
。如果它确实等于0
,那么你就需要回家去等红帽子小孩儿,也就是调用recvCond
的Wait
方法。这里使用的依然是for
语句。
如果信箱里有情报,那么你就应该取走情报,关上信箱、锁上锁,然后离开。对应的代码是mailbox = 0
和lock.RUnlock()
。之后,你还需要让蓝帽子小孩儿准时去我家楼下路过。这样我就知道信箱中的情报已经被你获取了。
以上这些,就是对咱们俩要执行秘密任务的代码实现。其中的条件变量的用法需要你特别注意。
再强调一下,只要条件不满足,我就会通过调用sendCond
变量的Wait
方法,去等待你的通知,只有在收到通知之后我才会再次检查信箱。
另外,当我需要通知你的时候,我会调用recvCond
变量的Signal
方法。你使用这两个条件变量的方式正好与我相反。你可能也看出来了,利用条件变量可以实现单向的通知,而双向的通知则需要两个条件变量。这也是条件变量的基本使用规则。
你可以打开demo61.go文件,看到上述例子的全部实现代码。
总结
我们这两期的文章会围绕条件变量的内容展开,条件变量是基于互斥锁的一种同步工具,它必须有互斥锁的支撑才能发挥作用。 条件变量可以协调那些想要访问共享资源的线程。当共享资源的状态发生变化时,它可以被用来通知被互斥锁阻塞的线程。我在文章举了一个两人访问信箱的例子,并用代码实现了这个过程。
思考题
*sync.Cond
类型的值可以被传递吗?那sync.Cond
类型的值呢?
感谢你的收听,我们下期再见。
Go语言核心36讲29的更多相关文章
- Go语言核心36讲(Go语言实战与应用三)--学习笔记
25 | 更多的测试手法 在本篇文章,我会继续为你讲解更多更高级的测试方法.这会涉及testing包中更多的 API.go test命令支持的,更多标记更加复杂的测试结果,以及测试覆盖度分析等等. 前 ...
- Go语言核心36讲(导读)--学习笔记
目录 开篇词 | 跟着学,你也能成为Go语言高手 导读 | 写给0基础入门的Go语言学习者 导读 | 学习专栏的正确姿势 开篇词 | 跟着学,你也能成为Go语言高手 Go 语言是由 Google 出品 ...
- Go语言核心36讲(Go语言进阶技术八)--学习笔记
14 | 接口类型的合理运用 前导内容:正确使用接口的基础知识 在 Go 语言的语境中,当我们在谈论"接口"的时候,一定指的是接口类型.因为接口类型与其他数据类型不同,它是没法被实 ...
- Go语言核心36讲(Go语言进阶技术十六)--学习笔记
22 | panic函数.recover函数以及defer语句(下) 我在前一篇文章提到过这样一个说法,panic 之中可以包含一个值,用于简要解释引发此 panic 的原因. 如果一个 panic ...
- Go语言核心36讲(Go语言实战与应用一)--学习笔记
23 | 测试的基本规则和流程 (上) 在接下来的日子里,我将带你去学习在 Go 语言编程进阶的道路上,必须掌握的附加知识,比如:Go 程序测试.程序监测,以及 Go 语言标准库中各种常用代码包的正确 ...
- Go语言核心36讲(Go语言实战与应用四)--学习笔记
26 | sync.Mutex与sync.RWMutex 从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包.这会涉及这些代码包的标准用法.使用禁忌.背后原理以及周边的知识. ...
- Go语言核心36讲(Go语言实战与应用七)--学习笔记
29 | 原子操作(上) 我们在前两篇文章中讨论了互斥锁.读写锁以及基于它们的条件变量,先来总结一下. 互斥锁是一个很有用的同步工具,它可以保证每一时刻进入临界区的 goroutine 只有一个.读写 ...
- 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 ...
随机推荐
- Docker_删除none镜像
Docker 删除 none 镜像 docker images|grep none|awk '{print $3}'|xargs docker rmi
- 使用Steamwork.Net 接入Steam一点心得
1. 前言 这是我在开发过程中使用的一点总结,目前使用的东西包含基础登录功能,存档功能,成就系统,以及DLC安装功能.Steamwork不仅仅有这些功能还有游戏内交易,排行榜,数据传输等功能,这些功 ...
- 多版本并发控制 MVCC
介绍多版本并发控制 多版本并发控制技术(Multiversion Concurrency Control,MVCC) 技术是为了解决问题而生的,通过 MVCC 我们可以解决以下几个问题: 读写之间阻塞 ...
- acme.sh官方中文说明文档
转载自:https://github.com/acmesh-official/acme.sh/wiki/说明 acme.sh 实现了 acme 协议, 可以从 letsencrypt 生成免费的证书. ...
- Kubernetes Operator: Operator
Operator 就可以看成是 CRD 和 Controller 的一种组合特例,Operator 是一种思想,它结合了特定领域知识并通过 CRD 机制扩展了 Kubernetes API 资源,使用 ...
- 使用Gitlab的CI/CD功能自动化推送docker镜像到Nexus仓库出现的问题
在服务器中可以直接使用命令行登录,推送docker镜像等 但是在使用Gitlab的CI/CD功能中,gitlab-ci.yml文件执行过程中出现如下错误: 原因分析: 服务器上之前使用命令行登陆过Ne ...
- JavaScript 的闭包(closure)
以下内容为本人的学习笔记,如需要转载,请声明原文链接微信公众号「englyf」https://www.cnblogs.com/englyf/ 对于闭包的理解,其实可以归纳为,在创建函数时,同时创建了一 ...
- 基于python的端口扫描
前言 端口扫描是指某些别有用心的人发送一组端口扫描消息,试图以此侵入某台计算机,并了解其提供的计算机网络服务类型(这些网络服务均与端口号相关).端口扫描是计算机解密高手喜欢的一种方式.攻击者可以通过它 ...
- 0-mysql数据库下载及安装
1 下载mysql源安装包 wget http://dev.mysql.com/get/mysql57-community-release-el7-8.noarch.rpm 2 安装mysql源 yu ...
- CentOS 7.9 安装 MySQL 5.7.35
CentOS 7.9 安装 MySQL 5.7.35 1 下载地址:https://downloads.mysql.com/archives/community/ 2 mysql5.7.35 安装包上 ...