前言
在并发编程中,条件变量是一种常见的同步机制,它允许 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
调用Signal
或Broadcast
来唤醒它。Signal
:唤醒一个等待该条件的goroutine
。如果有多个goroutine
在等待,Signal
只会唤醒一个。Broadcast
:唤醒所有等待该条件的goroutine
。NewCond
:创建一个新的sync.Cond
,并指定关联的互斥锁。
工作原理
sync.Cond
通常与互斥锁结合使用,锁用于保护共享资源,条件变量用于在特定条件下协调 goroutine
。具体流程如下:
- 一个
goroutine
在某个条件不满足时调用Wait
,它会释放锁并进入等待状态。 - 当另一个
goroutine
改变了共享资源的状态并使条件满足时,它会调用Signal
或Broadcast
来唤醒一个或多个等待中的goroutine
。 - 被唤醒的
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
方法内部机制
- 把调用它的(当前的)
goroutine
加入到当前条件变量的通知队列中 - 解锁当前的条件变量基于的互斥锁
- 让当前的
goroutine
处于等待状态,等到通知到来时再决定是否唤醒它。此时,这个goroutine
就会阻塞在调用Wait
方法的那行代码上 - 通知到来且决定唤醒这个
goroutine
,那就在唤醒之后重新锁定当前条件变量基于的互斥锁。当前的goroutine
就会继续执行后面的代码了。
Signal
和 Broadcast
区别
Signal
唤醒一个为此等待的goroutine
,Broadcast
唤醒所有为此等待的goroutine
Wait
方法会把当前的goroutine
加入到通知队列的队尾,Signal
总会从通知队列的队首开始查找可被唤醒的goroutine
。换句话说,一般都是最早等待的那一个Signal
和Broadcast
与Wait
方法不同,不需要在互斥锁的保护下进行,最好在解锁条件变量基于的那个互斥锁后调用。
总结
sync.Cond
是一个强大的同步工具,适用于需要协调多个 goroutine
之间依赖条件的场景。它可以与互斥锁结合使用,提供一种灵活的机制来在某些条件满足时唤醒等待的 goroutine
。 在实际开发中,合理使用条件变量能够有效地解决并发冲突,提高程序的性能和稳定性。