Go 条件变量:同一把锁、条件循环与关闭
本轮只做理论正文与源码静态复核;下列命令、示例和测试均未执行。已有输出按代码推演或历史记录理解,不作为本轮运行证据。
前置:并发基础与测试和竞态检测。先掌握锁与共享状态,再处理条件等待。channel 能清楚表达的简单通信优先使用 channel;当多个 goroutine 需要等待由共享状态表达的条件时,再考虑 sync.Cond。
Wait 等待的是条件
cond.L 必须与观察和修改条件的锁相同。调用 Wait 前先持有它;Wait 会释放锁并阻塞,返回前重新加锁。唤醒只表示“可以重新检查条件”,不是保证条件一定满足,所以要在 for !condition { cond.Wait() } 中等待。
Signal 唤醒一个等待者,Broadcast 唤醒所有等待者。它们不保存一份供未来等待者领取的通知,状态必须由受锁保护的字段记录。Cond 使用后不能复制。
完整示例:可关闭队列
package main
import (
"fmt"
"sync"
)
type Queue struct {
mu sync.Mutex
cond *sync.Cond
tasks []int
closed bool
}
func NewQueue() *Queue {
q := &Queue{}
q.cond = sync.NewCond(&q.mu)
return q
}
func (q *Queue) Put(value int) bool {
q.mu.Lock()
defer q.mu.Unlock()
if q.closed { return false }
q.tasks = append(q.tasks, value)
q.cond.Signal()
return true
}
func (q *Queue) Get() (int, bool) {
q.mu.Lock()
defer q.mu.Unlock()
for len(q.tasks) == 0 && !q.closed { q.cond.Wait() }
if len(q.tasks) == 0 { return 0, false }
value := q.tasks[0]
q.tasks = q.tasks[1:]
return value, true
}
func (q *Queue) Close() {
q.mu.Lock()
defer q.mu.Unlock()
q.closed = true
q.cond.Broadcast()
}
func main() {
q := NewQueue()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for {
n, ok := q.Get()
if !ok { return }
fmt.Println(n)
}
}()
for i := 1; i <= 3; i++ { q.Put(i) }
q.Close()
wg.Wait()
fmt.Println("closed")
}预期依次输出 1、2、3、closed。队列关闭后拒绝新任务,消费者先读完已有任务再返回。练习代码没有容量上限,真实任务队列需要按实际负载考虑背压。
容易出错的锁与退出
如果构造时写 sync.NewCond(&sync.Mutex{}),方法却锁 q.mu,Wait 释放的就不是当前持有的那把锁,会产生严重错误。不能让表面相同类型的两把锁分别保护同一条件。
资源池仅等待一个令牌时,有缓冲 channel 通常更直接;一次性的“所有工作开始”信号可关闭一个 channel。Cond 本身没有 context 取消参数,采用前应确定状态变更、唤醒和关闭方案,不使用 Sleep 假装确保等待者已经就位。
练习与验收
增加空队列立即关闭、多消费者排空、关闭后 Put 失败的测试;多消费者输出顺序不作固定假设,只检查任务不丢失且所有消费者最终结束。API 依据:sync.Cond。
练习提示与答案
提示:将Get的等待条件同时代入空队列与closed状态;再看Close的Broadcast。
参考答案
空队列Close后Get返回(0,false),不继续Wait;已有任务先排空再返回false,Put返回false。多消费者每项只被取一次,但取得先后不保证打印先后;检查值的多重集合与所有消费者结束,不对调度顺序作固定断言。示例整数队列不是通用无界生产方案。
继续阅读
- 开发实践
- 知识专题
- 知识专题