当前位置:首页 > 文章列表 > Golang > Go教程 > Go sync.Cond 实现有界队列的等待条件

Go sync.Cond 实现有界队列的等待条件

来源:17golang原创 2026-10-15 08:42:11 0浏览 收藏

我第一次用 sync.Cond 做有界队列时,最容易误判的是“已经调用了 Signal,为什么消费者还是卡着”。后来排查发现,真正决定正确性的不是通知方法单独怎么写,而是状态、锁和等待条件是否始终放在同一个保护域里。

这篇只讨论三个边界:队列满时生产者等待,队列空时消费者等待,以及队列关闭后如何让两侧都能退出。官方资料:https://pkg.go.dev/sync#Cond

最小规则是:持有保护状态的锁检查条件,用 for 循环调用 Wait,状态改变后再通知对应等待者。关闭属于状态变化,不能只关闭一个入口而不唤醒另一侧。

先确认 Wait 的三个硬条件

sync.Cond 不是“睡眠一会儿再重试”的定时器。它关联一个 Locker,调用 Wait 时会原子地释放这把锁并挂起当前 goroutine,恢复前又会重新拿锁。因此,调用前必须已经持有 Cond.L。

第二个条件是:返回并不代表业务条件一定成立。另一个 goroutine 可能先拿到锁并消费了数据,也可能只是同一条件下的其他等待者被唤醒,所以判断必须写在循环里。

// 这个骨架只展示 Cond 的正确等待边界,不承载具体队列逻辑。
mu.Lock()
for !condition() {
    // Wait 会暂时释放 mu,唤醒后重新持有 mu 再返回。
    cond.Wait()
}
// 这里才允许读取或修改由 mu 保护的状态。
useProtectedState()
mu.Unlock()

如果现场看到 Wait 外面只有一个 if,第一步就应该把它改成 for,再继续判断通知是否遗漏。这个顺序能避免把“偶尔卡住”误判成调度问题。

Go 有界队列中未满条件、非空条件、互斥锁与两个 sync.Cond 的静态关系图
图1:有界队列用同一把锁保护长度和关闭状态,再分别用 notFull、notEmpty 表达生产者与消费者的等待条件。

把有界队列拆成未满和非空

有界队列同时存在两个相反方向的等待:生产者只在“未满”时才能追加,消费者只在“非空”时才能取出。用两个条件变量能让通知目标更清楚:成功入队后通知消费者,成功出队后通知生产者。

package main

import (
    "errors"
    "sync"
)

var ErrClosed = errors.New("queue is closed")

type Queue[T any] struct {
    mu       sync.Mutex
    items    []T
    capacity int
    closed   bool
    notFull  *sync.Cond // 队列满时,Put 在这里等待。
    notEmpty *sync.Cond // 队列空时,Get 在这里等待。
}

func NewQueue[T any](capacity int) *Queue[T] {
    if capacity 

这里的关键不是字段多,而是每个字段都只有一把锁负责。len(q.items)、q.closed 的读取,追加和移除,都发生在 q.mu 保护下;两个 Cond 只是等待和通知的入口,不自己保存队列状态。

为什么 Signal 不能替代状态判断

Signal 只负责唤醒一个等待者,不负责把条件变成真。假设两个消费者都在等非空,生产者入队一个元素后调用一次 Signal,只有一个消费者应该拿到这个元素,另一个消费者被唤醒也必须重新看到队列仍为空并继续等待。

同理,队列从容量为 1 的满状态变成有一个空位时,通知一个生产者通常就够了。只有当状态变化可能让多个等待者都需要重新判断,或者关闭让所有等待者都有退出机会时,才使用 Broadcast。

状态变化推荐通知重新检查的条件
追加一个元素notEmpty.Signal()队列是否仍有元素、是否已关闭
移除一个元素notFull.Signal()队列是否仍有空位、是否已关闭
进入关闭状态两个 Cond 都 Broadcast()关闭状态与队列剩余元素

关闭时要同时唤醒两侧等待者

关闭是最容易漏通知的地方。只给消费者调用 Broadcast,可能让“队列已满、正在等空位”的生产者永久睡眠;只唤醒生产者也会留下空队列消费者。关闭动作应该持有同一把锁,先写入 closed,再同时唤醒两类等待者。

func (q *Queue[T]) Close() {
    q.mu.Lock()
    defer q.mu.Unlock()

    if q.closed {
        return
    }
    // 先发布关闭状态,再唤醒所有等待者,让它们各自判断退出条件。
    q.closed = true
    q.notFull.Broadcast()
    q.notEmpty.Broadcast()
}

Get 不能一看到关闭就丢掉剩余元素。上面的实现允许消费者先把已有元素取完,只有“已关闭且为空”时才返回 ErrClosed。Put 则在重新拿到锁后立即拒绝新元素,这样关闭后的行为是固定的。

Go 有界队列关闭状态、Put 等待者、Get 等待者和 Broadcast 通知的静态关系图
图2:关闭把 closed 状态写入同一保护域,并同时唤醒两侧等待者,让 Put 和 Get 都能重新判断退出条件。

按现象排查:到底是哪一层没有闭环

现象一:一调用 Wait 就崩溃或行为异常

先看调用前是否拿到了 Cond.L。如果创建 Cond 时传入的是一把锁,后续却用另一把锁包围状态判断,等待和通知就不再围绕同一个保护域工作。最稳妥的写法是把锁和 Cond 放在同一个结构体中,并让所有入口都调用同一把锁。

现象二:偶尔取到空值,或者刚唤醒又继续卡住

检查 Wait 是否被写成 if,以及唤醒后是否跳过了 closed 和队列长度判断。Wait 返回只是“可以重新竞争锁”,不是“你的业务条件已经兑现”。

现象三:关闭后程序迟迟不退出

把所有可能阻塞的入口列出来:满队列里的 Put、空队列里的 Get、以及可能等待二次条件的包装方法。关闭必须修改共享状态并广播所有相关 Cond;如果只有一侧退出,另一侧仍可能永久等待。

现象四:通知越来越多但吞吐没有改善

不要把 Broadcast 当成“更保险的 Signal”。广播会唤醒一批 goroutine,它们随后要重新竞争同一把锁;如果一次只新增一个元素,大量唤醒只会制造竞争。优先使用精确的 Signal,把 Broadcast 留给关闭或确实影响所有等待者的状态变更。

Cond 什么时候值得用

如果只是等待一个事件,channel 往往更直观:发送表示一次事件,关闭 channel 表示永久广播。sync.Cond 更适合“共享状态本身很复杂,等待者需要在拿锁后检查多个字段”的场景,例如本例的容量、长度和关闭状态必须同时判断。

我的判断标准是:如果等待条件可以自然地变成一个 channel 协议,就优先用 channel;如果必须让多个操作围绕同一份可变状态保持原子关系,再考虑 Cond。无论选择哪种方式,都要明确关闭语义、剩余数据策略和等待者如何退出。

最后的检查清单

  • 每个 Cond 的 L 是否就是保护条件状态的那把锁。
  • 每次 Wait 是否位于 for 循环中。
  • 入队、出队和关闭是否都在同一把锁下修改状态。
  • 入队后是否通知非空,出队后是否通知未满。
  • 关闭是否广播所有可能睡眠的 Cond。
  • 关闭后的 Get 是否明确“读完剩余数据再退出”还是“立即丢弃”。

相关问题

为什么 Wait 返回后还要再次判断队列长度?

因为通知只代表条件可能发生变化,返回后还要重新拿锁确认当前状态;其他 goroutine 可能已经先消费或占用了这次变化。

有多个消费者时一定要 Broadcast 吗?

不一定。一次只增加一个元素时,Signal 通常足够;关闭或条件变化影响所有等待者时才需要 Broadcast。

有界队列能不能直接用两个 channel?

可以,channel 通常更适合固定容量和单向事件模型。Cond 的价值在于等待条件需要和多字段共享状态一起判断。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go sync.Once 初始化失败后的重试设计Go sync.Once 初始化失败后的重试设计
上一篇
Go sync.Once 初始化失败后的重试设计
Go atomic.Int64 统计并发指标的读取一致性
下一篇
Go atomic.Int64 统计并发指标的读取一致性
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    479次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    535次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    557次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    506次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    328次使用