sync.Cond不是容器,不能替代队列;它仅是配合互斥锁实现条件等待与唤醒的协调工具,必须与显式共享状态(如切片)及*sync.Mutex共同使用,且Wait前须持锁、返回后需用for循环重检条件。

sync.Cond 不是高并发队列的“优化手段”,而是实现阻塞式队列调度逻辑的底层协调工具——它本身不存数据、不管理队列结构,只负责在条件变化时安全地挂起或唤醒 goroutine。
为什么不能直接用 sync.Cond 替代队列?
sync.Cond 是条件变量,不是容器。你没法往 cond 里 append 元素,也不能调用 cond.Len()。它必须和一个显式的共享状态(比如切片、链表、环形缓冲区)配合使用,且该状态必须由配套的 *sync.Mutex 或 *sync.RWMutex 保护。
常见误用:把 sync.Cond 当成带通知能力的 chan,结果发现没有缓冲、无法传递值、甚至 panic 报错 cond.Wait() called with no lock held。
- 必须先
cond.L.Lock()才能调用cond.Wait() -
cond.Signal()和cond.Broadcast()前无需加锁,但修改共享状态(如向 buffer 写入)时必须持有同一把锁 - Wait 返回后,锁已自动重新获取,但条件可能已被其他 goroutine 消费,所以必须用
for循环重检,不能用if
sync.Cond 在阻塞队列中的典型调度模式
以生产者-消费者模型为例,核心逻辑围绕「缓冲区是否非空」或「容量是否未满」展开。这里的「条件」不是魔法,而是你明确定义的布尔表达式,比如 len(q.items) > 0 或 len(q.items) 。
消费者等待逻辑示例:
func (q *Queue) Get() Item {
q.cond.L.Lock()
defer q.cond.L.Unlock()
for len(q.items) == 0 {
q.cond.Wait() // 释放锁、挂起,被 Signal 后自动重锁
}
item := q.items[0]
q.items = q.items[1:]
return item
}
生产者通知逻辑示例:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
func (q *Queue) Put(item Item) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
q.items = append(q.items, item)
q.cond.Signal() // 只唤醒一个等待的消费者
}
- 若队列支持多消费者同时取多个元素(如
GetN(n int)),等待条件应为len(q.items) >= n,仍需循环检查 - 若生产者关闭队列,应改用
cond.Broadcast()并配合closed bool状态位,避免遗漏唤醒 - 注意:Signal 不保证唤醒「最先等待」的 goroutine,Go 调度器不提供 FIFO 保证
和 chan 对比:什么场景下值得换用 sync.Cond?
标准 chan 已足够处理大多数生产者-消费者场景。只有当出现以下需求时,sync.Cond 才有不可替代性:
- 需要基于**复合条件**等待:例如「缓冲区长度 ≥ 5 且最后一条是日志级别 ERROR」,
chan无法表达这种逻辑 - 需要**一对多广播唤醒**且不希望创建大量 channel:比如配置热更新完成,通知所有监听模块重载,用
Broadcast()比遍历发送更轻量 - 已有复杂状态结构(如带 TTL 的缓存池、优先级队列),不想/不能套进
chan的数据流模型中 - 对唤醒时机有精细控制:例如仅当新元素满足某谓词时才唤醒,而
chan 是无条件触发
性能上,sync.Cond 的 Wait/Sinal 调用开销略低于 chan 的收发(无内存拷贝、无 runtime.chansend 额外路径),但差异通常在纳秒级,不应作为选型主因。
容易忽略的线程安全细节
最常踩的坑不在 Cond 本身,而在共享状态与锁的边界不一致:
- 切片
q.items的append操作可能引发底层数组扩容并生成新地址,若此时其他 goroutine 正在读取旧 slice 头部,会看到不一致视图 —— 必须确保所有读写都发生在Lock()/Unlock()之间 - 如果用
*sync.RWMutex,注意cond.Wait()要求的是可写锁(Lock()),不能用RUnlock()配对,否则 panic -
cond.Signal()调用后,被唤醒 goroutine 不会立即执行;它要等当前持有锁的 goroutineUnlock()后才能抢到锁并继续 —— 这个延迟是设计使然,不是 bug - 不要在
Wait()前做耗时操作(如网络请求、磁盘 IO),否则阻塞了锁,拖慢所有协程
真实系统中,sync.Cond 往往藏在封装好的队列结构内部,对外暴露的是 Put/Get 方法。它的价值不在于炫技,而在于让「等待-通知」这一动作,在复杂状态判断下依然保持正确性和可维护性。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










