Cond的概念
Cond 原语的目的是,为等待 / 通知场景下的并发问题提供支持。Cond 通常应用于等待某个条件的一组 goroutine,等条件变为 true 的时候,其中一个 goroutine 或者所有的 goroutine 都会被唤醒执行。
从开发实践上,我们真正使用 Cond 的场景比较少,因为一旦遇到需要使用 Cond 的场景,我们更多地会使用 Channel 的方式去实现,因为那才是更地道的 Go 语言的写法
Cond的方法
Wait 方法:会把调用者 Caller 放入 Cond 的等待队列中并阻塞,直到被 Signal 或者 Broadcast 的方法从等待队列中移除并唤醒。
调用 Wait 方法时必须要持有 c.L 的锁。
- 必须先持有锁
- 当前 goroutine 挂起(进入等待队列)
- 自动释放锁
- 被唤醒后 → 重新抢锁
- 返回
- Wait = 解锁 + 休眠 + 被唤醒后加锁
Signal 方法,允许调用者 Caller 唤醒一个等待此 Cond 的 goroutine。如果此时没有等待的 goroutine,显然无需通知 waiter;如果 Cond 等待队列中有一个或者多个等待的 goroutine,则需要从等待队列中移除第一个 goroutine 并把它唤醒。在其他编程语言中,比如 Java 语言中,Signal 方法也被叫做 notify 方法。 唤醒 一个 等待者
调用 Signal 方法时,不强求你一定要持有 c.L 的锁。
不会释放锁,只是发通知
Broadcast 方法,允许调用者 Caller 唤醒所有等待此 Cond 的 goroutine。如果此时没有等待的 goroutine,显然无需通知 waiter;如果 Cond 等待队列中有一个或者多个等待的 goroutine,则清空所有等待的 goroutine,并全部唤醒。在其他编程语言中,比如 Java 语言中,Broadcast 方法也被叫做 notifyAll 方法。
调用 Broadcast 方法时,也不强求你一定持有 c.L 的锁。
不会释放锁,只是发通知
Cond的使用
package main
import (
"fmt"
"sync"
"time"
)
type Queue struct {
mu sync.Mutex
cond *sync.Cond
items []int
}
func NewQueue() *Queue {
q := &Queue{}
q.cond = sync.NewCond(&q.mu)
return q
}
func (q *Queue) Push(v int) {
q.mu.Lock()
q.items = append(q.items, v)
q.cond.Signal() // 唤醒一个等待的 Pop
q.mu.Unlock()
}
func (q *Queue) Pop() int {
q.mu.Lock()
for len(q.items) == 0 {
q.cond.Wait()
}
v := q.items[0]
q.items = q.items[1:]
q.mu.Unlock()
return v
}
func main() {
queue := NewQueue()
var wg sync.WaitGroup
// 生产者 goroutine
wg.Add(1)
go func() {
defer wg.Done()
for i := 1; i <= 5; i++ {
fmt.Printf("生产者: 推送 %d\n", i)
queue.Push(i)
time.Sleep(100 * time.Millisecond) // 模拟生产间隔
}
}()
// 消费者 goroutine
wg.Add(1)
go func() {
defer wg.Done()
for i := 1; i <= 5; i++ {
value := queue.Pop()
fmt.Printf("消费者: 弹出 %d\n", value)
time.Sleep(150 * time.Millisecond) // 模拟消费时间
}
}()
// 等待所有goroutine完成
wg.Wait()
fmt.Println("所有操作完成")
// 更复杂的测试场景 - 多个消费者
fmt.Println("\n=== 多消费者测试 ===")
queue2 := NewQueue()
// 启动多个消费者
for i := 0; i < 3; i++ {
wg.Add(1)
go func(consumerID int) {
defer wg.Done()
// 每个消费者尝试获取2个元素
for j := 0; j < 2; j++ {
value := queue2.Pop()
fmt.Printf("消费者 %d: 弹出 %d\n", consumerID, value)
time.Sleep(50 * time.Millisecond)
}
}(i)
}
// 生产者
wg.Add(1)
go func() {
defer wg.Done()
// 生产6个元素
for i := 1; i <= 6; i++ {
time.Sleep(80 * time.Millisecond) // 控制生产节奏
fmt.Printf("生产者: 推送 %d\n", i)
queue2.Push(i)
}
}()
wg.Wait()
fmt.Println("多消费者测试完成")
// 测试广播功能
fmt.Println("\n=== Broadcast测试 ===")
broadcastQueue := NewQueue()
// 启动3个等待的消费者
for i := 0; i < 3; i++ {
wg.Add(1)
go func(waiterID int) {
defer wg.Done()
fmt.Printf("等待者 %d: 开始等待\n", waiterID)
value := broadcastQueue.Pop()
fmt.Printf("等待者 %d: 收到值 %d\n", waiterID, value)
}(i)
}
// 等待所有消费者进入等待状态
time.Sleep(2 * time.Second)
// 推送一个值,应该唤醒一个等待者
fmt.Println("推送第一个值...")
broadcastQueue.Push(999)
// 再次等待
time.Sleep(4 * time.Second)
// 推送更多值以满足其他等待者
for i := 1; i <= 2; i++ {
broadcastQueue.Push(1000 + i)
time.Sleep(100 * time.Millisecond)
}
wg.Wait()
fmt.Println("Broadcast测试完成")
}基本使用方法
-
创建Cond
mu := sync.Mutex{} cond := sync.NewCond(&mu)
Cond必须绑定一个锁(Mutex或RWMutex) -
等待条件(必须用 for)
cond.L.Lock() for !condition { cond.Wait() } doSomething() cond.L.Unlock()
必须是
for,不能是if -
修改条件 + 通知
cond.L.Lock() condition = true cond.Signal() // 或 cond.Broadcast() cond.L.Unlock()
注意事项
-
用 if 而不是 for(严重 bug)
// 错误 if len(q.items) == 0 { cond.Wait() }
原因:
- 可能被“误唤醒”(spurious wakeup)
- 多个 goroutine 被
Broadcast同时唤醒 - 条件可能再次不满足
// 正确 for !condition { cond.Wait() }
-
Wait 前没加锁(直接 panic)❌
Wait内部会调用Unlock(),没锁直接炸。 -
修改条件不加锁(数据竞争)❌
condition = true cond.Signal()
必须包在同一把锁里。
-
Signal 在 Unlock 之后调用(逻辑错误)❌
-
期望 Signal 精确唤醒“某个” goroutine ❌
Cond 不保证公平性、不保证顺序。
cond和channel该怎么选
| 场景 | 推荐 |
|---|---|
| 简单通知、一次性事件 | channel |
| 生产者 / 消费者(简单) | channel |
| 多条件 + 状态反复变化 | sync.Cond |
| 广播唤醒 | sync.Cond |
| 实现并发数据结构 | sync.Cond |
总结
sync.Cond 是 Go 中用于 基于共享条件的协程等待与唤醒机制,
适合 多条件、广播通知、并发结构实现 的场景,
使用时必须 锁 + for + Wait/Signal 原子配合,
一旦用错,不是死锁就是竞态。