背景
sync.Cond 是 Go 语言中的一个同步原语,用于实现多个 goroutine 之间的条件同步。它可以让一个或多个 goroutine 等待某个条件满足,而另一个 goroutine 在条件满足时可以通知它们继续执行。
sync.Cond 的主要应用场景有:
- 并发访问数据库或共享内存时的同步
- 等待特定任务完成后的同步
- 控制并发 goroutine 数量
用法
使用 sync.Cond 的基本步骤如下:
-
创建一个 sync.Cond 实例,通常需要传入一个 locker 对象,如 mutex。
-
在需要等待条件的 goroutine 中,调用 cond.Wait() 方法,该 goroutine 会阻塞在这里。
-
在条件满足时,在另一个 goroutine 中调用 cond.Signal() 方法,通知等待的 goroutine 唤醒继续执行。
-
被 Notify 的 goroutine 从 Wait() 返回后就可以继续往下执行了。
一个简单的示例:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// 共享队列
var queue = make([]int, 0)
// 队列最大容量
const queueCap = 5
var mutex sync.Mutex
var cond = sync.NewCond(&mutex)
// 生产者: 生成随机数放入队列
func producer() {
for {
mutex.Lock()
for len(queue) == queueCap {
cond.Wait()
}
randNum := rand.Intn(100)
queue = append(queue, randNum)
fmt.Println("Produce:", randNum)
mutex.Unlock()
cond.Signal()
time.Sleep(time.Second)
}
}
// 消费者:从队列中取出数据
func consumer() {
for {
mutex.Lock()
for len(queue) == 0 {
cond.Wait()
}
num := queue[0]
queue = queue[1:]
fmt.Println("Consume:", num)
mutex.Unlock()
cond.Signal()
time.Sleep(time.Second)
}
}
func main() {
go producer()
go consumer()
time.Sleep(10 * time.Second)
}
需要注意:
- cond.Wait() 需要在锁保护下调用,并在返回前解锁,避免死锁。
- 同一个 Cond 可以重复使用,但尽量避免信号丢失的情况。
- 可以通过 Broadcast() 一次性唤醒所有等待的 goroutine。
原理
sync.Cond 的核心是维护一个等待条件的 goroutine 计数器。 等待条件的 goroutine 通过 Wait() 使计数器加 1,并阻塞自身。 另一个 goroutine 通过 Signal() 使计数器减 1,从而唤醒被阻塞的 goroutine。
具体实现:
-
Cond 在创建时会初始化一个计数器 notifyList。
-
Wait() 将当前 goroutine 加入 notifyList,并解锁等待条件满足。
-
Signal() 将 notifyList 中的一个 goroutine 唤醒。
-
Broadcast() 将 notifyList 中的所有 goroutine 唤醒。
-
当计数器为 0 时,会自动广播唤醒。
这样通过计数器和阻塞的方式实现了条件变量的语义,使得多个 goroutine 可以进行同步。
详情请看源码:
package sync
import (
"sync/atomic"
"unsafe"
)
type Cond struct {
// L 是在观察或更改条件时持有的锁。
L Locker
// notify 是一个通知列表,用于唤醒等待条件的 goroutine。
notify notifyList
// checker 是一个复制检查器,用于检测 Cond 对象是否被复制。
checker copyChecker
}
// NewCond 返回一个新的 Cond 实例,其中 Locker 是 Cond 实例的写锁。
func NewCond(l Locker) *Cond {
return &Cond{
L: l,
}
}
// Wait 使一个 goroutine 等待条件满足。
func (c *Cond) Wait() {
c.checker.check()
t := runtime_notifyListAdd(&c.notify)
c.L.Unlock()
runtime_notifyListWait(&c.notify, t)
c.L.Lock()
}
// Signal 唤醒一个等待中的 goroutine。
func (c *Cond) Signal() {
c.checker.check()
runtime_notifyListNotifyOne(&c.notify)
}
// Broadcast 唤醒所有等待中的 goroutine。
func (c *Cond) Broadcast() {
c.checker.check()
runtime_notifyListNotifyAll(&c.notify)
}
// copyChecker 持有一个指向自身的指针,用于检测 Cond 对象是否被复制。
type copyChecker uintptr
func (c *copyChecker) check() {
if uintptr(*c)!= uintptr(unsafe.Pointer(c)) &&!atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) && uintptr(*c)!= uintptr(unsafe.Pointer(c)) {
panic("sync.Cond is copied")
}
}
type noCopy struct{}
// Lock 是一个空操作,用于 -copylocks 检查器从 'go vet' 中检查 Cond 对象是否被复制。
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
小结
sync.Cond 是 Go 中实现条件变量同步的利器,使用相对简单,通过计数器和阻塞的方式实现了条件同步的语义。在并发程序中如果需要等待某条件才能继续执行,可以考虑使用 sync.Cond。