云原生探索系列(十七):Go 语言sync.Cond

前言

在并发编程中,条件变量是一种常见的同步机制,它允许 goroutine 在某个条件满足时继续执行。比如你要做一顿丰盛的晚餐, 大概分为3个步骤,首先,熬汤,然后,做主菜,最后,上菜,主菜不能提前做,因为它容易冷掉,必须等汤煮好之后才做。 在这个过程中,做主菜的任务依赖于汤煮好的条件。若你在汤还没煮好的时候开始做主菜,主菜可能会做得过早而变凉。这就需要你在做主菜之前等待汤煮好。 那该如何知道汤已经煮好了,可以做主菜了?

Go 语言提供了 sync.Cond 类型来实现这一功能, sync.Cond 是一个用于协调多个 goroutine 之间的通信和同步的工具。 sync.Cond 就像一个信号器,在"汤煮好"的时候,通知等待的 goroutine (比如做主菜的那个人)继续执行。

什么是 sync.Cond?

sync.Cond 是 Go 标准库 sync 包中的一个结构体,它提供了一个条件变量的实现,允许 goroutine 在等待某个条件时挂起,直到条件满足时被唤醒。

sync.Cond 实际上是与互斥锁(sync.Mutex )或读写锁(sync.RWMutex )结合使用的,通常它通过锁来保护共享状态,并使用条件变量来协调 goroutine 之间的执行顺序。

基础用法

  • Wait :使当前 goroutine 等待条件满足。在调用 Wait 之前,必须持有 sync.Cond 关联的锁。调用 Wait 会释放该锁,并使当前 goroutine 进入阻塞状态,直到其他 goroutine 调用 SignalBroadcast 来唤醒它。
  • Signal :唤醒一个等待该条件的 goroutine 。如果有多个 goroutine 在等待, Signal 只会唤醒一个。
  • Broadcast :唤醒所有等待该条件的 goroutine
  • NewCond :创建一个新的 sync.Cond ,并指定关联的互斥锁。

工作原理

sync.Cond 通常与互斥锁结合使用,锁用于保护共享资源,条件变量用于在特定条件下协调 goroutine 。具体流程如下:

  1. 一个 goroutine 在某个条件不满足时调用 Wait ,它会释放锁并进入等待状态。
  2. 当另一个 goroutine 改变了共享资源的状态并使条件满足时,它会调用 SignalBroadcast 来唤醒一个或多个等待中的 goroutine
  3. 被唤醒的 goroutine 会重新尝试获取锁,并在锁可用时继续执行。

使用示例

我们来实现,文章开头做晚餐的案例:

erlang 复制代码
package main

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

type Meal struct {
	cond *sync.Cond
}

func NewMeal() *Meal {
	m := &Meal{}
	var lock sync.Mutex
	m.cond = sync.NewCond(&lock) // 通过互斥锁创建条件变量
	return m
}

func (m *Meal) CookSoup() {
	fmt.Println("开始煮汤...")
	time.Sleep(3 * time.Second)
	fmt.Println("汤煮好了!")
	m.cond.Signal()
}

func (m *Meal) CookMainDish() {
	fmt.Println("做主菜,请等待汤煮好...")
	m.cond.L.Lock()
	m.cond.Wait()
	m.cond.L.Unlock()

	fmt.Println("汤煮好了,可以开始做主菜了!")
}

func main() {
	meal := NewMeal()

	go meal.CookMainDish()

	go meal.CookSoup()

	time.Sleep(5 * time.Second)
}

这段代码中:

  • Meal 结构体:包含了一个条件变量 cond ,用于协调"煮汤"和"做主菜"之间的关系。
  • CookSoup 方法:模拟煮汤的过程,煮好汤后,调用 Signal() 唤醒等待的主菜 goroutine
  • CookMainDish 方法:首先等待汤煮好(通过 cond.Wait() ),直到条件满足才能继续做主菜。 执行后,输出内容如下:
erlang 复制代码
做主菜,请等待汤煮好...
开始煮汤...
汤煮好了!
汤煮好了,可以开始做主菜了!

这个场景的核心:

  • 条件等待与通知: sync.Cond 允许 goroutine 在某个条件未满足时进入阻塞等待,直到其他 goroutine 满足条件并通过 Signal()Broadcast() 唤醒等待的 goroutine
  • 协调多个任务:就像在做晚餐时,"做主菜"的人不能在"汤煮好"之前开始工作, sync.Cond 在这里就充当了协调者,确保任务的顺序。

Wait 方法内部机制

  1. 把调用它的(当前的) goroutine 加入到当前条件变量的通知队列中
  2. 解锁当前的条件变量基于的互斥锁
  3. 让当前的 goroutine 处于等待状态,等到通知到来时再决定是否唤醒它。此时,这个 goroutine 就会阻塞在调用 Wait 方法的那行代码上
  4. 通知到来且决定唤醒这个 goroutine ,那就在唤醒之后重新锁定当前条件变量基于的互斥锁。当前的 goroutine 就会继续执行后面的代码了。

SignalBroadcast 区别

  • Signal 唤醒一个为此等待的 goroutineBroadcast 唤醒所有为此等待的 goroutine
  • Wait 方法会把当前的 goroutine 加入到通知队列的队尾, Signal 总会从通知队列的队首开始查找可被唤醒的 goroutine 。换句话说,一般都是最早等待的那一个
  • SignalBroadcastWait 方法不同,不需要在互斥锁的保护下进行,最好在解锁条件变量基于的那个互斥锁后调用。

总结

sync.Cond 是一个强大的同步工具,适用于需要协调多个 goroutine 之间依赖条件的场景。它可以与互斥锁结合使用,提供一种灵活的机制来在某些条件满足时唤醒等待的 goroutine。 在实际开发中,合理使用条件变量能够有效地解决并发冲突,提高程序的性能和稳定性。

相关推荐
爱勇宝43 分钟前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
AskHarries1 小时前
工具失败时怎么办:重试、回滚、人工确认和风险提示
后端·程序员
苏三说技术3 小时前
Claude Code从失控到起飞,只用了这些技巧
后端
长栎3 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode4 小时前
Redis 在生产项目的使用
前端·后端
用户559822481224 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode4 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战4 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha4 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn4 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端