Go sync.Cond 实现有界队列的等待条件
我第一次用 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,再继续判断通知是否遗漏。这个顺序能避免把“偶尔卡住”误判成调度问题。

把有界队列拆成未满和非空
有界队列同时存在两个相反方向的等待:生产者只在“未满”时才能追加,消费者只在“非空”时才能取出。用两个条件变量能让通知目标更清楚:成功入队后通知消费者,成功出队后通知生产者。
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 则在重新拿到锁后立即拒绝新元素,这样关闭后的行为是固定的。

按现象排查:到底是哪一层没有闭环
现象一:一调用 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 的价值在于等待条件需要和多字段共享状态一起判断。
Go sync.Once 初始化失败后的重试设计
- 上一篇
- Go sync.Once 初始化失败后的重试设计
- 下一篇
- Go atomic.Int64 统计并发指标的读取一致性
-
- Golang · Go教程 | 30分钟前 |
- Go os/signal 处理服务退出信号的生命周期
- 322浏览 收藏
-
- Golang · Go教程 | 32分钟前 | Go crypto/tls 证书到期时间 x509.Certificate NotAfter
- Go crypto/tls 读取证书到期时间的代码写法
- 183浏览 收藏
-
- Golang · Go教程 | 35分钟前 |
- Go net/netip 解析地址并区分 IPv4 映射
- 377浏览 收藏
-
- Golang · Go教程 | 38分钟前 |
- Go container/heap 实现定时任务优先队列
- 395浏览 收藏
-
- Golang · Go教程 | 41分钟前 | Go 切片排序 稳定排序 sort.SliceStable
- Go sort.SliceStable 保持同键顺序的排序实现
- 300浏览 收藏
-
- Golang · Go教程 | 42分钟前 | 配置 · 错误处理 · Go教程 · Go 超时配置 time.Duration time.ParseDuration 配置时长
- Go time.ParseDuration 读取配置时长的错误处理
- 119浏览 收藏
-
- Golang · Go教程 | 44分钟前 | goroutine · go · Context · Go 定时任务 reset goroutine泄漏 time.Ticker Stop
- Go time.Ticker 长时间服务的停止与泄漏规避
- 121浏览 收藏
-
- Golang · Go教程 | 45分钟前 |
- Go time.Location 处理业务时区的转换方法
- 239浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 479次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 535次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 557次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 506次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 328次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- 基于Golang实现延迟队列(DelayQueue)
- 2023-02-16 321浏览
-
- Go+Redis实现延迟队列实操
- 2023-02-23 426浏览
-
- Go保证并发安全底层实现详解
- 2023-02-24 417浏览
-
- Go语言开发保证并发安全实例详解
- 2023-01-07 328浏览
