前言
在并发编程中,条件变量是一种常见的同步机制,它允许 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唤醒所有为此等待的goroutineWait方法会把当前的goroutine加入到通知队列的队尾,Signal总会从通知队列的队首开始查找可被唤醒的goroutine。换句话说,一般都是最早等待的那一个Signal和Broadcast与Wait方法不同,不需要在互斥锁的保护下进行,最好在解锁条件变量基于的那个互斥锁后调用。
总结
sync.Cond 是一个强大的同步工具,适用于需要协调多个 goroutine 之间依赖条件的场景。它可以与互斥锁结合使用,提供一种灵活的机制来在某些条件满足时唤醒等待的 goroutine。 在实际开发中,合理使用条件变量能够有效地解决并发冲突,提高程序的性能和稳定性。