本篇文章面向的读者: 已经基本掌握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() 为什么不行?

秒懂 Golang 中的 条件变量(sync.Cond)的更多相关文章

  1. 条件变量 sync.Cond

    sync.Cond 条件变量是基于互斥锁的,它必须有互斥锁的支撑才能发挥作用. sync.Cond 条件变量用来协调想要访问共享资源的那些线程,当共享资源的状态发生变化的时候,它可以用来通知被互斥锁阻 ...

  2. 在SQL存储过程中给条件变量加上单引号

    在SQL存储过程中给条件变量加上单引号,不加语句就会出问题,以下就是在存储过程中将条件where设置成了动态变化的,给where赋完值再和前面的语句拼接,再execute(SQL) ), )), )+ ...

  3. linux中的条件变量

    1 大家可能知道互斥量是线程程序中必须的工具了,但是也不能是万能的,就比如某个线程正在等待共享数据某个条件的发生,这个时候会发生什么呢.它就可能重复的尝试对互斥对象锁定和解锁来检查共享数据结构. 2 ...

  4. 练习生产者与消费者-PYTHON多线程中的条件变量同步-Queue

    以前练习过,但好久不用,手生,概念也生了, 重温一下.. URL: http://www.cnblogs.com/holbrook/tag/%E5%A4%9A%E7%BA%BF%E7%A8%8B/ ~ ...

  5. go中sync.Cond源码解读

    sync.Cond 前言 什么是sync.Cond 看下源码 Wait Signal Broadcast 总结 sync.Cond 前言 本次的代码是基于go version go1.13.15 da ...

  6. 转载~kxcfzyk:Linux C语言多线程库Pthread中条件变量的的正确用法逐步详解

    Linux C语言多线程库Pthread中条件变量的的正确用法逐步详解   多线程c语言linuxsemaphore条件变量 (本文的读者定位是了解Pthread常用多线程API和Pthread互斥锁 ...

  7. go条件变量的使用和原理

    场景 最近写代码时碰到一个场景, 需要使用 map[int]struct{} 结构来存储task, map的key是task的id,随时可以增减.因为的确除了看书,基本上没使用过条件变量所以后面过了一 ...

  8. Linux 多线程条件变量同步

    条件变量是线程同步的另一种方式,实际上,条件变量是信号量的底层实现,这也就意味着,使用条件变量可以拥有更大的自由度,同时也就需要更加小心的进行同步操作.条件变量使用的条件本身是需要使用互斥量进行保护的 ...

  9. [转]Posix-- 互斥锁 条件变量 信号量

    这是一个关于Posix线程编程的专栏.作者在阐明概念的基础上,将向您详细讲述Posix线程库API.本文是第三篇将向您讲述线程同步. 互斥锁 尽管在Posix Thread中同样可以使用IPC的信号量 ...

  10. Linux同步机制(二) - 条件变量,信号量,文件锁,栅栏

    1 条件变量 条件变量是一种同步机制,允许线程挂起,直到共享数据上的某些条件得到满足. 1.1 相关函数 #include <pthread.h>  pthread_cond_t cond ...

随机推荐

  1. MVVM视图模型

  2. 齐博x1标签之无刷新显示更多

    示范代码如下: <div class="ListMoreInfos"> {qb:tag name="news_list_page_listdata02&quo ...

  3. go channel原理及使用场景

    转载自:go channel原理及使用场景 源码解析 type hchan struct { qcount uint // Channel 中的元素个数 dataqsiz uint // Channe ...

  4. Paxos分布式系统共识算法?我愿称其为点歌算法…

    原创:微信公众号 码农参上,欢迎分享,转载请保留出处. 哈喽大家好啊,我是Hydra. 分布式系统共识算法Paxos相信大家都不陌生,它被称为最难理解的算法不是没有道理的,首先,它的发表之路就充满了坎 ...

  5. nrf9160 做modem—— 连接云(接入方式MQTT)

    今天测试把nrf9160作为modem的例程Serial LTE Modem程序(后面简称slm),何为做modem,通俗来说就是将nrf9160作为无线模块,主控由其余MCU做,主控通过AT命令控制 ...

  6. letcode刷题记录-day01-两数之和

    题目:两数之和 描述 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标. 你可以假设每种输入只会对应一 ...

  7. 抓包分析 TCP 握手和挥手

    前言 首先需要明确的是 TCP 是一个可靠传输协议,它的所有特点最终都是为了这个可靠传输服务.在网上看到过很多文章讲 TCP 连接的三次握手和断开连接的四次挥手,但是都太过于理论,看完感觉总是似懂非懂 ...

  8. 论文笔记 - GRAD-MATCH: A Gradient Matching Based Data Subset Selection For Efficient Learning

    Analysis Coreset 是带有权重的数据子集,目的是在某个方面模拟完整数据的表现(例如损失函数的梯度,既可以是在训练数据上的损失,也可以是在验证数据上的损失): 给出优化目标的定义: $w^ ...

  9. day12-Servlet02

    Servlet02 6.GET和POST请求的分发处理 开发Servlet,通常编写doGet,doPost方法.来对表单的get和post请求进行分发处理 例子 在web文件夹下面创建一个html页 ...

  10. centos使用lftp备份文件

    一直以来项目的文件没有备份,最近需要增加备份,本来以为备份是IT的工作,结果IT说工作忙,拖了半个月给分配完ftp服务器后说不给备份,需要我们开发自己备份...我特么*** 对于ftp备份,咱是没有经 ...