条件变量的使用(golang)

1、背景

最近在学习go的一个开源协程池,在源码中有用到锁、信号量,锁相对来说用的是比较多的,信号量相对用的较少,之前研究学习过c++的std::condition_variable,其实和golang的大同小异,个人感觉c++的略强大一些,其wait有两个重载,第二个语法糖的实现主要是为了防止虚假唤醒。

2、介绍

条件变量的使用场景是,当满足某个条件时,触发某个动作,类似于排队打饭或者挂号看病,当医生或者打饭员准备好后,就触发看病或者打饭的动作。

条件变量有三个函数:

Go 复制代码
con.Wait()

该函数会将当前协程放入等待队列并阻塞,直到Signal或Broadcast方法将协程从等待队列中移除并唤醒。

Go 复制代码
con.Signal()

唤醒等待队列中的一个子协程,先唤醒最先阻塞的子协程,被唤醒之后,子协程继续执行。

Go 复制代码
con.Broadcast()

Broadcast功能类似Signal,不同的是signal只唤醒一个子协程,而broadcast是唤醒所有阻塞的子协程。

3、使用示例

3.1 signal

我们就以在食堂排队打饭为例子,假如本次排队有10位同学排队,这10位同学分别编号为0-9号(就不具体起名字了),当打饭师傅准备好后,就开始排队打饭,示例代码如下:

Go 复制代码
package main

import (
	"fmt"
	"sync"
	"time"
)

func DaFan(x int, num *int, wg *sync.WaitGroup, cond *sync.Cond) {
	defer wg.Done()
	// 当有一个lock成功后,其他协程阻塞在这里
	cond.L.Lock()
	*num++
	fmt.Println("学号为:", x, "的同学排到了第:", *num, "位")
	cond.Wait()
	fmt.Println("学号为:", x, "的同学打饭了")
	cond.L.Unlock()
}
func main() {
	var wg sync.WaitGroup
	num := 0
	var lock sync.Mutex
	con := sync.NewCond(&lock)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go DaFan(i, &num, &wg, con)
	}
	// 模拟打饭准备
	time.Sleep(5 * time.Second)
	// 循环通知这10位同学打饭
	for i := 0; i < 10; i++ {
		con.Signal()
		// 打饭耗时1秒
		time.Sleep(1 * time.Second)
	}
	wg.Wait()
}

上面代码运行结果如下:

3.2 broadcast

broadcast为广播,好比将军一声令下,所有士兵齐刷刷的冲向敌阵,broadcast的应用场景如下:

将军下令,要求全部将士在10s内修整完成,15s后发起总攻,假如我们有十个兵团,分别为1-10号:

Go 复制代码
package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func GeneralAttack(x int, num *int, wg *sync.WaitGroup, cond *sync.Cond) {
	defer wg.Done()
	// 随机一个10秒内的时间用于修整
	time.Sleep(time.Second * time.Duration(rand.Intn(10)))
	fmt.Println("兵团", x+1, "在", time.Now().Format("2006-01-02 15:04:05"), "准备完毕,等待总攻")
	cond.L.Lock()
	// 准备完毕,等待教练发令
	cond.Wait()
	cond.L.Unlock()
	fmt.Println("兵团", x+1, "开始发起总攻", time.Now().Format("2006-01-02 15:04:05"))
}
func main() {
	var wg sync.WaitGroup
	num := 0
	var lock sync.Mutex
	con := sync.NewCond(&lock)
	fmt.Println("将军下令,开始修整:", time.Now().Format("2006-01-02 15:04:05"), ",15s后开始总攻")
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go GeneralAttack(i, &num, &wg, con)
	}
	// 等待发起总攻
	time.Sleep(15 * time.Second)
	fmt.Println("将军下令,开始总攻:", time.Now().Format("2006-01-02 15:04:05"))
	// 发起总攻
	con.Broadcast()
	wg.Wait()
}

运行记录:

Go 复制代码
liupeng@liupengdeMacBook-Pro test % go run main.go
将军下令,开始修整: 2024-04-06 21:04:32 ,15s后开始总攻
兵团 2 在 2024-04-06 21:04:32 准备完毕,等待总攻
兵团 1 在 2024-04-06 21:04:32 准备完毕,等待总攻
兵团 4 在 2024-04-06 21:04:33 准备完毕,等待总攻
兵团 9 在 2024-04-06 21:04:33 准备完毕,等待总攻
兵团 6 在 2024-04-06 21:04:37 准备完毕,等待总攻
兵团 3 在 2024-04-06 21:04:38 准备完毕,等待总攻
兵团 8 在 2024-04-06 21:04:39 准备完毕,等待总攻
兵团 10 在 2024-04-06 21:04:39 准备完毕,等待总攻
兵团 5 在 2024-04-06 21:04:40 准备完毕,等待总攻
兵团 7 在 2024-04-06 21:04:41 准备完毕,等待总攻
将军下令,开始总攻: 2024-04-06 21:04:47
兵团 6 开始发起总攻 2024-04-06 21:04:47
兵团 7 开始发起总攻 2024-04-06 21:04:47
兵团 2 开始发起总攻 2024-04-06 21:04:47
兵团 3 开始发起总攻 2024-04-06 21:04:47
兵团 8 开始发起总攻 2024-04-06 21:04:47
兵团 10 开始发起总攻 2024-04-06 21:04:47
兵团 5 开始发起总攻 2024-04-06 21:04:47
兵团 1 开始发起总攻 2024-04-06 21:04:47
兵团 9 开始发起总攻 2024-04-06 21:04:47
兵团 4 开始发起总攻 2024-04-06 21:04:47
liupeng@liupengdeMacBook-Pro test % 

如上运行记录,十个兵团在10s内准备完毕,准备完成后等待将军下令发起总攻,每个兵团就是一个协程,并行开始准备,现实情况也是如此,每个兵团准备时间不同,但都在上级要求的时间内准备完成,之后等待将军下令。

使用 条件变量,在调用 Wait() 方法之前,必须要加锁,否则代码会导致程序 panic。原因是调用 Wait 方法,会先把调用者放入等待队列中,然后释放锁。此时如果在未持有锁时调用释放锁的方法,就会导致程序 panic。

相关推荐
VBA63372 分钟前
VBA数据库解决方案第十五讲:Recordset集合中单个数据的精确处理
开发语言
wrx繁星点点6 分钟前
事务的四大特性(ACID)
java·开发语言·数据库
不写八个13 分钟前
Python办公自动化教程(005):Word添加段落
开发语言·python·word
HEX9CF17 分钟前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
赵荏苒42 分钟前
Python小白之Pandas1
开发语言·python
丶Darling.44 分钟前
代码随想录 | Day26 | 二叉树:二叉搜索树中的插入操作&&删除二叉搜索树中的节点&&修剪二叉搜索树
开发语言·数据结构·c++·笔记·学习·算法
人生の三重奏1 小时前
前端——js补充
开发语言·前端·javascript
计算机学姐1 小时前
基于SpringBoot+Vue的高校运动会管理系统
java·vue.js·spring boot·后端·mysql·intellij-idea·mybatis
平凡的小码农1 小时前
JAVA实现大写金额转小写金额
java·开发语言
yttandb1 小时前
重生到现代之从零开始的C语言生活》—— 内存的存储
c语言·开发语言·生活