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

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

我们常常会把条件变量这个同步工具拿来与互斥锁一起讨论。实际上,条件变量是基于互斥锁的,它必须有互斥锁的支撑才能发挥作用。

条件变量并不是被用来保护临界区和共享资源的,它是用于协调想要访问共享资源的那些线程的。当共享资源的状态发生变化时,它可以被用来通知被互斥锁阻塞的线程。

比如说,我们两个人在共同执行一项秘密任务,这需要在不直接联系和见面的前提下进行。我需要向一个信箱里放置情报,你需要从这个信箱中获取情报。这个信箱就相当于一个共享资源,而我们就分别是进行写操作的线程和进行读操作的线程。

如果我在放置的时候发现信箱里还有未被取走的情报,那就不再放置,而先返回。另一方面,如果你在获取的时候发现信箱里没有情报,那也只能先回去了。这就相当于写的线程或读的线程阻塞的情况。

虽然我们俩都有信箱的钥匙,但是同一时刻只能有一个人插入钥匙并打开信箱,这就是锁的作用了。更何况咱们俩是不能直接见面的,所以这个信箱本身就可以被视为一个临界区。

尽管没有协调好,咱们俩仍然要想方设法的完成任务啊。所以,如果信箱里有情报,而你却迟迟未取走,那我就需要每过一段时间带着新情报去检查一次,若发现信箱空了,我就需要及时地把新情报放到里面。

另一方面,如果信箱里一直没有情报,那你也要每过一段时间去打开看看,一旦有了情报就及时地取走。这么做是可以的,但就是太危险了,很容易被敌人发现。

后来,我们又想了一个计策,各自雇佣了一个不起眼的小孩儿。如果早上七点有一个戴红色帽子的小孩儿从你家楼下路过,那么就意味着信箱里有了新情报。另一边,如果上午九点有一个戴蓝色帽子的小孩儿从我家楼下路过,那就说明你已经从信箱中取走了情报。

这样一来,咱们执行任务的隐蔽性高多了,并且效率的提升非常显著。这两个戴不同颜色帽子的小孩儿就相当于条件变量,在共享资源的状态产生变化的时候,起到了通知的作用。

当然了,我们是在用 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方法。你使用这两个条件变量的方式正好与我相反。你可能也看出来了,利用条件变量可以实现单向的通知,而双向的通知则需要两个条件变量。这也是条件变量的基本使用规则。

看到上述例子的全部实现代码

package main

import (
"log"
"sync"
"time"
) func main() {
// mailbox 代表信箱。
// 0代表信箱是空的,1代表信箱是满的。
var mailbox uint8
// lock 代表信箱上的锁。
var lock sync.RWMutex
// sendCond 代表专用于发信的条件变量。
sendCond := sync.NewCond(&lock)
// recvCond 代表专用于收信的条件变量。
recvCond := sync.NewCond(lock.RLocker()) // sign 用于传递演示完成的信号。
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 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) <-sign
<-sign
}

总结

我们这两期的文章会围绕条件变量的内容展开,条件变量是基于互斥锁的一种同步工具,它必须有互斥锁的支撑才能发挥作用。 条件变量可以协调那些想要访问共享资源的线程。当共享资源的状态发生变化时,它可以被用来通知被互斥锁阻塞的线程。我在文章举了一个两人访问信箱的例子,并用代码实现了这个过程。

思考题

*sync.Cond类型的值可以被传递吗?那sync.Cond类型的值呢?

笔记源码

https://github.com/MingsonZheng/go-core-demo

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

Go语言核心36讲(Go语言实战与应用五)--学习笔记的更多相关文章

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

    24 | 测试的基本规则和流程(下) Go 语言是一门很重视程序测试的编程语言,所以在上一篇中,我与你再三强调了程序测试的重要性,同时,也介绍了关于go test命令的基本规则和主要流程的内容.今天我 ...

  2. Go语言核心36讲(Go语言基础知识三)--学习笔记

    03 | 库源码文件 在我的定义中,库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 Go 语言规范的话). 这里的"其他代码" ...

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

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

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

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

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

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

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

    34 | 并发安全字典sync.Map (上) 我们今天再来讲一个并发安全的高级数据结构:sync.Map.众所周知,Go 语言自带的字典类型map并不是并发安全的. 前导知识:并发安全字典诞生史 换 ...

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

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

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

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

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

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

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

    46 | 访问网络服务 前导内容:socket 与 IPC 人们常常会使用 Go 语言去编写网络程序(当然了,这方面也是 Go 语言最为擅长的事情).说到网络编程,我们就不得不提及 socket. s ...

随机推荐

  1. Python3入门系列之-----异常处理

    前言 作为 Python 初学者,在刚学习 Python 编程时,经常会看到一些报错信息,在前面我们没有提及,这章节我们会专门介绍. Python 有两种错误很容易辨认:语法错误和异常. Python ...

  2. AngularJS的简单实用

    Angular Js 的初步认识和使用    一:        1.模块化            定义模块和控制器  ng-app="myapp"  controller=&qu ...

  3. 11.4.4 LVS-Fullnat

    lvs-fullnat(双向转换) 通过请求报文的源地址为DIP,目标为RIP来实现转发:对于响应报文而言,修改源地址为VIP,目标地址为CIP来实现转发: CIP --> DIP VIP -- ...

  4. 手机淘宝轻店业务 Serverless 研发模式升级实践

    一.前言 随着 Serverless 在业界各云平台落地,阿里内部 Serverless 研发平台.各种研发模式也在业务中逐步落地,如火如荼.在此契机下,淘系团队启动了轻店 Serverless 研发 ...

  5. 使用tinypng对需要上传Gitee图床的图片进行压缩

    目录 背景 Tinypng简介 Tinypng使用 手动上传图片 使用API 调用API自动上传超过1MB图片 安装tinyfy 自动上传脚本 其他 背景 在使用Gitee作为图床时(使用Typora ...

  6. 解决pip._vendor.urllib3.exceptions.ReadTimeoutError: HTTPSConnectionPool(host='files.pythonhosted.org', port=443): Read timed out.

    参考链接[侵权删] https://www.jianshu.com/p/3378fa827924 https://yq.aliyun.com/articles/619208 问题描述:在Windows ...

  7. javascript-jquery插件

    1.jquery创建插件 jQuery.extend({插件名:函数体,插件名:函数体}): html部分 <div id="div1">开始动画</div> ...

  8. 半天撸一个简易版mybatis

    为什么需要持久层框架? 首先我们先看看使用原生jdbc存在的问题? public static void main(String[] args) { Connection connection = n ...

  9. Spring Cloud Gateway Route Predicate Factory 的使用

    Spring Cloud Gateway的使用 一.需求 二.基本组成 1.简介 2.核型概念 1.Route 路由 2.Predicate 谓语.断言 3.Filter 过滤器 3.工作原理 三.网 ...

  10. FastAPI 学习之路(二十九)使用(哈希)密码和 JWT Bearer 令牌的 OAuth2

    既然我们已经有了所有的安全流程,就让我们来使用 JWT 令牌和安全哈希密码让应用程序真正地安全. 关于 JWT 它是一个将 JSON 对象编码为密集且没有空格的长字符串的标准.字符串看起来像这样: e ...