Skip to content
← 返回Go列表
开发实践

Go 条件变量:同一把锁、条件循环与关闭 ​

本轮只做理论正文与源码静态复核;下列命令、示例和测试均未执行。已有输出按代码推演或历史记录理解,不作为本轮运行证据。

前置:并发基础与测试和竞态检测。先掌握锁与共享状态,再处理条件等待。channel 能清楚表达的简单通信优先使用 channel;当多个 goroutine 需要等待由共享状态表达的条件时,再考虑 sync.Cond。

Wait 等待的是条件 ​

cond.L 必须与观察和修改条件的锁相同。调用 Wait 前先持有它;Wait 会释放锁并阻塞,返回前重新加锁。唤醒只表示“可以重新检查条件”,不是保证条件一定满足,所以要在 for !condition { cond.Wait() } 中等待。

Signal 唤醒一个等待者,Broadcast 唤醒所有等待者。它们不保存一份供未来等待者领取的通知,状态必须由受锁保护的字段记录。Cond 使用后不能复制。

完整示例:可关闭队列 ​

go
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。多消费者每项只被取一次,但取得先后不保证打印先后;检查值的多重集合与所有消费者结束,不对调度顺序作固定断言。示例整数队列不是通用无界生产方案。

继续阅读