Golang 中的 条件变量(sync.Cond)详解
作者:muguang_lijing 发布时间:2024-04-25 15:28:32
本篇文章面向的读者: 已经基本掌握Go中的 协程(goroutine),通道(channel),互斥锁(sync.Mutex),读写锁(sync.RWMutex) 这些知识。如果对这些还不太懂,可以先回去把这几个知识点解决了。
首先理解以下三点再进入正题:
Go中的一个协程 可以理解成一个独立的人,多个协程是多个独立的人
多个协程都需要访问的 共享资源(比如共享变量) 可以理解成 多人要用的某种公共社会资源
上锁 其实就是加入到某个共享资源的争抢组中;上锁完成 就是从争抢组中被选出,得到了期待的共享资源;解锁 就是退出某个共享资源的争抢组。
假如有这样一个现实场景:在一个公园中有一个公共厕所,这个厕所一次只能容纳一个人上厕所,同时这个厕所中有个放卷纸的位置,其一次只能放一卷纸,一卷纸的总长度是 5 米,而每个人上一次厕所需要用掉 1 米的纸。而当一卷纸用完后,公园管理员要负责给厕所加上一卷新纸,以便大家可以继续使用厕所。 那么对于这个单人公共厕所,大家只能排队上厕所,当每个人进到厕所的时候,当然会把厕所门锁好,以便任何人都进不来(包括管理员)。管理员若要进到厕所查看用纸情况并加卷纸,也需要排队(因为插队总是不文明对吧)。
那么怎么用 Golang 去模拟上述场景呢?
首先我们先不用 sync.Cond,看如何实现?那么请看下面这段代码:
package main
import (
"fmt"
"time"
"sync"
)
var 卷纸 int
var m sync.Mutex
var wg sync.WaitGroup
func 上厕所(姓名 string){
m.Lock()
defer func(){
m.Unlock()
wg.Done()
}()
fmt.Printf("%s 进到厕所\t",姓名)
if 卷纸 >= 1 { // 进到厕所第一件事是看还有没有纸
fmt.Printf("正在拉屎中...\n")
time.Sleep(time.Second)
卷纸 -= 1
fmt.Printf("%s 已用完厕所,正在离开\n",姓名)
return
}
fmt.Printf("发现纸用完了,无奈先离开厕所\n")
}
func 加厕纸(){
m.Lock()
defer func(){
m.Unlock()
wg.Done()
}()
fmt.Printf("公园管理员 进到厕所\t")
if 卷纸 <= 0 { // 管理员进到厕所是看纸有没有用完
fmt.Printf("公园管理员 正在加新纸...\n")
time.Sleep(time.Millisecond*500)
卷纸 = 5
fmt.Printf("公园管理员 已加上新厕纸,正在离开\n")
}else{
fmt.Printf("发现纸还没用完,先离开厕所\n")
}
}
func main() {
卷纸 = 5 // 厕所一开始就准备好了一卷纸,长度5米
要排队上厕所的人 := [...]string{"老王","小李","老张","小刘","阿明","欣欣","西西","芳芳"}
for _,谁 := range 要排队上厕所的人 {
wg.Add(1)
go 上厕所(谁)
}
wg.Add(1)
go 加厕纸()
wg.Wait()
}
/*
输出(由于协程执行顺序的不可预测性,因此每次输出的顺序都可能不一样):
公园管理员 进到厕所 发现纸还没用完,先离开厕所
阿明 进到厕所 正在拉屎中...
阿明 已用完厕所,正在离开
老王 进到厕所 正在拉屎中...
老王 已用完厕所,正在离开
小刘 进到厕所 正在拉屎中...
小刘 已用完厕所,正在离开
小李 进到厕所 正在拉屎中...
小李 已用完厕所,正在离开
老张 进到厕所 正在拉屎中...
老张 已用完厕所,正在离开
欣欣 进到厕所 发现纸用完了,无奈先离开厕所
芳芳 进到厕所 发现纸用完了,无奈先离开厕所
西西 进到厕所 发现纸用完了,无奈先离开厕所
*/
上面的代码已经能看出一些效果,但还是有问题:最后三个人因为厕纸用完,都直接离开厕所后就没有后续了?应该是他们离开厕所后再次尝试排队,直到需求解决,就离开厕所不再参与排队了,否则要不断去排队上厕所。而公园管理员呢,他要一直去排队进到厕所里看还有没有纸,而不是看一次就再也不管了。 那么请看下面的完善代码:
package main
import (
"fmt"
"sync"
"time"
)
var (
卷纸 int
m sync.Mutex
wg sync.WaitGroup
厕所的排队 chan string
)
func 上厕所(姓名 string) {
m.Lock() // 该语句的调用只说明本执行体(可理解成该姓名所指的那个人)加入到了厕所资源的争抢组中;
// 而该语句的完成调用,才代表了从争抢组中脱颖而出,抢到了厕所;在完成调用之前,会一直阻塞在这里(可理解为这个人正在争抢中)
defer func() {
m.Unlock()
wg.Done()
}()
fmt.Printf("%s 进到厕所\t", 姓名)
if 卷纸 >= 1 { // 进到厕所第一件事是看还有没有纸
fmt.Printf("正在拉屎中...\n")
time.Sleep(time.Second)
卷纸 -= 1
fmt.Printf("%s 已用完厕所,正在离开\n", 姓名)
return
}
fmt.Printf("发现纸用完了,无奈先离开厕所\n")
厕所的排队 <- 姓名 // 再次加入厕所排队,期望下次可以成功如厕
}
func 加厕纸() {
m.Lock()
defer m.Unlock()
fmt.Printf("公园管理员 进到厕所\t")
if 卷纸 <= 0 { // 管理员进到厕所是看纸有没有用完
fmt.Printf("公园管理员 正在加新纸...\n")
time.Sleep(time.Millisecond * 500)
卷纸 = 5
fmt.Printf("公园管理员 已加上新厕纸,正在离开\n")
} else {
fmt.Printf("发现纸还没用完,先离开厕所\n")
}
}
func main() {
卷纸 = 5 // 厕所一开始就准备好了一卷纸,长度5米
要上厕所的人 := [...]string{"老王", "小李", "老张", "小刘", "阿明", "欣欣", "西西", "芳芳"} // 这里只是举几个人名例子,假设此处有源源不断的人去上厕所(读者可以随意改造人名来源)
厕所的排队 = make(chan string, len(要上厕所的人))
for _, 谁 := range 要上厕所的人 {
厕所的排队 <- 谁
}
go func() { // 在这个执行体中,会不断从 厕所排队 中把人加入到 对厕所资源的争抢组中
for 谁 := range 厕所的排队 {
wg.Add(1)
go 上厕所(谁)
}
}()
wg.Add(1)
go func() { // 在这个执行体中,代表公园管理员的个人时间线,他会每隔一段时间去加入争抢组进到厕所,检查纸还有没有
for {
time.Sleep(time.Millisecond * 1200)
加厕纸()
}
}()
wg.Wait()
}
/*
输出:
老王 进到厕所 正在拉屎中...
老王 已用完厕所,正在离开
芳芳 进到厕所 正在拉屎中...
芳芳 已用完厕所,正在离开
阿明 进到厕所 正在拉屎中...
阿明 已用完厕所,正在离开
小刘 进到厕所 正在拉屎中...
小刘 已用完厕所,正在离开
欣欣 进到厕所 正在拉屎中...
欣欣 已用完厕所,正在离开
小李 进到厕所 发现纸用完了,无奈先离开厕所
老张 进到厕所 发现纸用完了,无奈先离开厕所
西西 进到厕所 发现纸用完了,无奈先离开厕所
公园管理员 进到厕所 公园管理员 正在加新纸...
公园管理员 已加上新厕纸,正在离开
西西 进到厕所 正在拉屎中...
西西 已用完厕所,正在离开
小李 进到厕所 正在拉屎中...
小李 已用完厕所,正在离开
老张 进到厕所 正在拉屎中...
老张 已用完厕所,正在离开
公园管理员 进到厕所 发现纸还没用完,先离开厕所
公园管理员 进到厕所 发现纸还没用完,先离开厕所
公园管理员 进到厕所 发现纸还没用完,先离开厕所
*/
上面这个代码在功能上基本是完善了,成功模拟了上述 多人上公厕 的场景。但仔细一想,这个场景其实有些地方是不合常理的:如果有个人进到厕所发现没纸,难道他会出来紧接着再去排队吗?如果排了三次五次甚至十次还是没有纸,还要这样不断地反复排队进去出来又排队?而公园管理员,要是这样不断反复排队进厕所查看,那么他这一天其他啥事都干不了。
所以更合理实际的情况应该是:如果一个人进到厕所发现没纸,他应该先去在旁边歇着或在附近干别的,当公园管理员加完纸后,会通过喇叭吆喝一声:“新纸已加上”。这样,附近所有因为没厕纸而歇着的人就会听到这个通知,此时,他们再去尝试排队进厕所;而公园管理员也不用不断去排队进厕所检查纸用完了没有,因为经过升级,厕所加装了一个功能,有一个纸用尽的报警按钮装在纸盒旁边,当上完厕所的人发现纸用完的时候,他会先按下这个报警按钮,再离开厕所。这个报警的声音在整个公园的各处都可以听到,所以管理员无论在哪里干啥,他都能收到这个纸用尽的报警信号,然后他才去进厕所加纸。
其实这种被动通知的模式就是 sync.Cond 的核心思想,它会减少资源消耗,达到更优的效果,下面就是改良为 sync.Cond 的实现代码:
package main
import (
"fmt"
"math"
"strconv"
"sync"
"time"
)
var (
卷纸 int
m sync.Mutex
cond = sync.NewCond(&m)
)
func 上厕所(姓名 string) {
m.Lock() // 该语句的调用只说明本执行体(可理解成该姓名所指的那个人)加入到了厕所资源的争抢组中;
// 而该语句的完成调用,才代表了从争抢组中脱颖而出,抢到了厕所;在完成调用之前,会一直阻塞在这里(可理解为这个人正在争抢中)
defer m.Unlock()
fmt.Printf("%s 进到厕所\t", 姓名)
for 卷纸 < 1 { // 进到厕所第一件事是看还有没有纸
fmt.Printf("发现纸用完了,先离开厕所在附近歇息等待信号\n")
cond.Wait() // 该语句的调用 相当于调用了 m.Unlock() 也就是退出了争抢组,而是先歇着等待纸加上的信号;
// 当收到纸加上的信号后,该语句会自动执行 m.Lock(),也就是会重新加入到厕所的争抢组中;
// 该语句的完成调用说明已经再次成功争抢到了厕所;
fmt.Printf("%s 等到了厕纸已加的信号,并去再次抢到了厕所\t", 姓名)
}
fmt.Printf("正在拉屎中...\n")
time.Sleep(time.Second)
卷纸 -= 1
fmt.Printf("%s 已用完厕所\t", 姓名)
if 卷纸 < 1 { // 注意这里:在他用完厕所离开前,他需要看是不是纸已经用完了,如果用完了,就按下纸用尽的报警按钮,给公园管理员发送信号
cond.Broadcast() // 想想,这里为什么不用 Signal() ?因为 Signal 只能通知到一个等待者,这样就有可能通知不到 公园管理员。可以试着把这里换成 Signal() 试下
fmt.Printf("发现厕纸已用完,并按下了报警\t")
}
fmt.Printf("正在离开厕所\n")
}
func 加厕纸() {
m.Lock()
defer m.Unlock()
fmt.Printf("公园管理员 进到厕所\t")
for 卷纸 > 0 { // 管理员进到厕所是看纸有没有用完
fmt.Printf("发现纸还没用完,先离开厕所在等纸用尽的报警消息\n")
cond.Wait() // 如果纸没用完,就先去干其他工作,等纸用尽的报警消息
fmt.Printf("公园管理员 等到了纸用尽的报警消息,并再次抢到了厕所\n")
}
fmt.Printf("公园管理员 正在加新纸...\n")
time.Sleep(time.Millisecond * 500)
卷纸 = 5
cond.Broadcast() // 注意:公园管理员加完新纸后,要通过喇叭喊一声 “纸已加上” 的消息通知所有 因没纸而等待上厕所的人
fmt.Printf("公园管理员 已加上新厕纸,并通过喇叭通知了该消息,并正在离开厕所\n")
}
func main() {
卷纸 = 5 // 厕所一开始就准备好了一卷纸,长度5米
要上厕所的人 := [...]string{"老王", "小李", "老张", "小刘", "阿明", "欣欣", "西西", "芳芳"} // 上厕所的人名模板
go func() { // 在这个执行体中,代表厕所及厕所队列的时间线,厕所永远运营下去
for i := 0; i < math.MaxInt; i++ { // 此循环通过编号加上上面的姓名模板来 创建源源不断 上厕所的人
for _, 人名模板 := range 要上厕所的人 {
谁 := 人名模板 + strconv.Itoa(i)
go 上厕所(谁)
time.Sleep(time.Millisecond * 500) // 平均每半秒有一个人去上厕所
}
fmt.Printf("\n====================>> 屏幕停止输出后,请按Enter键继续 <<====================\n\n")
fmt.Scanln()
}
}()
go func() { // 在这个执行体中,代表公园管理员的个人时间线,管理员永不退休
for {
// 注意:相比上个版本,此处不用再加 Sleep 函数了,因为 加厕纸() 函数中的 cond.Wait() 会在有纸的时候等待信号
加厕纸()
}
}()
end := make(chan bool)
<-end
}
/*
输出:
公园管理员 进到厕所 发现纸还没用完,先离开厕所在等纸用尽的报警消息
老王0 进到厕所 正在拉屎中...
老王0 已用完厕所 正在离开厕所
小李0 进到厕所 正在拉屎中...
小李0 已用完厕所 正在离开厕所
老张0 进到厕所 正在拉屎中...
老张0 已用完厕所 正在离开厕所
小刘0 进到厕所 正在拉屎中...
小刘0 已用完厕所 正在离开厕所
阿明0 进到厕所 正在拉屎中...
====================>> 屏幕停止输出后,请按Enter键继续 <<====================
阿明0 已用完厕所 发现厕纸已用完,并按下了报警 正在离开厕所
欣欣0 进到厕所 发现纸用完了,先离开厕所在附近歇息等待信号
西西0 进到厕所 发现纸用完了,先离开厕所在附近歇息等待信号
芳芳0 进到厕所 发现纸用完了,先离开厕所在附近歇息等待信号
公园管理员 等到了纸用尽的报警消息,并再次抢到了厕所
公园管理员 正在加新纸...
公园管理员 已加上新厕纸,并通过喇叭通知了该消息,并正在离开厕所
公园管理员 进到厕所 发现纸还没用完,先离开厕所在等纸用尽的报警消息
欣欣0 等到了厕纸已加的信号,并去再次抢到了厕所 正在拉屎中...
欣欣0 已用完厕所 正在离开厕所
芳芳0 等到了厕纸已加的信号,并去再次抢到了厕所 正在拉屎中...
芳芳0 已用完厕所 正在离开厕所
西西0 等到了厕纸已加的信号,并去再次抢到了厕所 正在拉屎中...
西西0 已用完厕所 正在离开厕所
老王1 进到厕所 正在拉屎中...
老王1 已用完厕所 正在离开厕所
小李1 进到厕所 正在拉屎中...
小李1 已用完厕所 发现厕纸已用完,并按下了报警 正在离开厕所
老张1 进到厕所 发现纸用完了,先离开厕所在附近歇息等待信号
公园管理员 等到了纸用尽的报警消息,并再次抢到了厕所
公园管理员 正在加新纸...
公园管理员 已加上新厕纸,并通过喇叭通知了该消息,并正在离开厕所
公园管理员 进到厕所 发现纸还没用完,先离开厕所在等纸用尽的报警消息
小刘1 进到厕所 正在拉屎中...
小刘1 已用完厕所 正在离开厕所
阿明1 进到厕所 正在拉屎中...
====================>> 屏幕停止输出后,请按Enter键继续 <<====================
*/
用了 sync.Cond 的代码显然要精简了很多,而且还节省了计算资源,只会在收到通知的时候 才去抢公共厕所,而不是不断地反复去抢公共厕所。通过这个对现实场景的模拟,我们就很容易从使用者的角度理解 sync.Cond 是什么,它的字面意思就是 “条件”,这就已经点出了这东西的核心要义,就是满足条件才执行,条件是什么,信号其实就是条件,当一个执行体收到信号之后,它才去争抢共享资源,否则就会挂起等待(这种等待底层其实会让出线程,所以这种等待并不会空耗资源),比起不断轮寻去抢资源,这种方式要节省得多。
最后留给读者一个思考的问题:就是上面最后一版的代码,为什么 当纸用完后按报警按钮通知 公园管理员 要用 sync.Broadcast() 方法去广播通知?不是只通知管理员一个人吗,单独通知他不就行了,用 sync.Signal() 为什么不行?
来源:https://www.cnblogs.com/muguanglj/p/16980853.html


猜你喜欢
- 本篇目标抓取淘宝MM的姓名,头像,年龄抓取每一个MM的资料简介以及写真图片把每一个MM的写真图片按照文件夹保存到本地熟悉文件保存的过程&nb
- LoadRunner监控MySQLhttp://www.docin.com/p-92272846.htmlAdvanced MySQL Pe
- 如何搭建完整的网站架构并设计出一个出色的网站?关于这个问题,我们很难提出一个绝对权威和正确的设计思路,但任何网站的设计都需要遵循一个循序渐进
- 国内镜像源pip如果不配置国内镜像源的话,下载包的速度非常慢,毕竟默认的源在国外呢,国内主要的镜像源有如下,其中我个人喜欢用阿里云的镜像源。
- 有的bug,莫名其妙就好了…python3.9 报错 “AttributeError: 'HTMLParser' objec
- os.system()和os.popen()概述大家搞python与操作系统交互时,必须掌握的两个方法就是os.system()和os.po
- SQL语言查询基础:连接查询 通过连接运算符可以实现多个表查询。连接是关系数据库模型的主要特点,也是它区别于其它类型数据库管理系
- 一、数据完整性简介1、数据完整性简介数据冗余是指数据库中存在一些重复的数据,数据完整性是指数据库中的数据能够正确反应实际情况。数据完整性是指
- 接着上一篇,统一思想,遵循标准。如何遵循标准,其实标准有很多,结构标准,表现标准,行为标准。选择标准规范,就优先选择W3C推荐的标准。结构标
- 本文实例讲述了thinkphp的URL路由规则与配置方法。分享给大家供大家参考。具体分析如下:一、URL规则1、默认是区分大小写的2、如果我
- 要在django项目中定期执行任务,比如每天一定的时间点抓取数据,刷新数据库等,可以参考stackoverflow的方法,先编写一个mana
- 二维列表转一维列表from compiler.ast import flattena=[[1,2],[5,6]]print(flatten(
- 安装cesium在已有项目中执行,npm i cesium修改配置build/webpack.base.conf.js1、定义 Cesium
- 1. 现在的日期时间命令是<%=now%> 即可2.ASP取得表格(from)数据输入的方法,是使用一个内置
- 背景刚入行的同学,看到在SQL语句中出现where 1 = 1这样的条件可能会有所困惑,而长时间这样使用的朋友可能又习以为常。那么,你是否还
- 前言:目前我们使用的绝大多数计算机程序,无论是办公软件,浏览器,甚至游戏、视频都是通过菜单界面系统配置的,它几乎成了我们使用机器的默认方式。
- 目录前言前期准备数据的选择与获取分词筛选与可视化总结前言”数据可视化“这个话题,相信大家并不陌生,在一些平台,经常可以看到一些动态条形图的视
- 本文实例讲述了python爬虫学习笔记之Beautifulsoup模块用法。分享给大家供大家参考,具体如下:相关内容:什么是beautifu
- 本文实例讲述了Python装饰器。分享给大家供大家参考。具体分析如下:这是在Python学习小组上介绍的内容,现学现卖、多练习是好的学习方式
- 使用PHP GD,使用良好,一键剪裁各种尺寸,打包下载。经常换icon的懂的,美工给你一个1024的logo,你得ps出各种尺寸,于是有了这