Go Cond 源码解析
一、介绍
sync.Cond 用于协调多个goroutine 等待/唤醒 的同步原语。
作用:让一组goroutine 等待某个"条件满足",当条件满足后,由其他goroutine 唤醒等待的goroutine。
二、结构体说明
go
type Cond struct {
noCopy noCopy // 禁止拷贝的标记(编译期检查)
// 观察或修改条件时,必须持有该锁
L Locker
notify notifyList // 等待队列,存储阻塞的 goroutine
checker copyChecker // 运行时检查是否拷贝了 Cond
}
| 字段 | 作用 |
|---|---|
| noCopy | 编译期禁止拷贝 Cond(通过 go vet 检查),拷贝会导致同步逻辑失效 |
| L | 关联的锁(Mutex/RWMutex),必须实现 Locker 接口(Lock/Unlock) |
| notify | 底层等待队列,存储所有调用 Wait() 阻塞的 goroutine |
| checker | 运行时检查 Cond 是否被拷贝,若拷贝会 panic |
注意:Cond首次使用后进行拷贝,拷贝会导致等待队列(notify)、锁(L)等核心资源的关联关系失效,引发并发安全问题(比如唤醒的是拷贝后的无效队列)
三、核心方法
| 方法 | 作用 |
|---|---|
| NewCond(l Locker) *Cond | 创建 Cond,必须传入关联的锁 |
| Wait() | 1. 释放关联的锁 L;2. 阻塞当前 goroutine;3. 被唤醒后重新获取锁L |
| Signal() | 唤醒等待队列中一个 goroutine |
| Broadcast() | 唤醒等待队列中所有 goroutine |
四、源码解析
4.1 NewCond(l Locker) *Cond
go
// NewCond returns a new Cond with Locker l.
func NewCond(l Locker) *Cond {
return &Cond{L: l}
}
创建并返回一个新的cond实例 ,关联传入的Locker类型参数l
仅初始化 Cond 的 L 字段,其他字段依赖零值初始化。
NewCond 是创建 sync.Cond 实例的唯一合法方式,强制要求传入关联的锁(Locker)
4.2 copyChecker
go
// copyChecker holds back pointer to itself to detect object copying.
// copyChecker 持有指向自身的反向指针,用于检测对象是否被拷贝
type copyChecker uintptr
func (c *copyChecker) check() {
// Check if c has been copied in three steps:
// 三步检查 c 是否被拷贝:
// 1. The first comparison is the fast-path. If c has been initialized and not copied, this will return immediately. Otherwise, c is either not initialized, or has been copied.
// 1. 快速路径:如果 c 已初始化且未拷贝,直接返回,;否则要么未初始化,要么已拷贝
// 2. Ensure c is initialized. If the CAS succeeds, we're done. If it fails, c was either initialized concurrently and we simply lost the race, or c has been copied.
// 2. 确保 c 初始化, CAS 成功则完成;失败则要么并发初始化竞争失败,要么已拷贝
// 3. Do step 1 again. Now that c is definitely initialized, if this fails, c was copied.
// 3. 再次执行第一步:此时 c 已确定初始化,若对比失败则说明被拷贝
if uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
!atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
uintptr(*c) != uintptr(unsafe.Pointer(c)) {
panic("sync.Cond is copied")
}
}
4.2.1 字段说明
copyChecker: 检测 sync.Cond 是否被非法拷贝 的工具类型,通过自身指针与存储值的对比实现拷贝检测;
uintptr: 无符号整数,可存储指针地址,初始化时会将自身的内存地址 存入值中
4.2.2 check() 方法详解
| 语法片段 | 说明 |
|---|---|
uintptr(*c) |
获取 copyChecker 存储的整数值(初始化后是自身的原始地址) |
uintptr(unsafe.Pointer(c)) |
将当前 copyChecker 的指针地址(&c)转为 uintptr 类型 |
atomic.CompareAndSwapUintptr |
原子 CAS 操作:比较并交换 uintptr 值,保证并发安全的初始化 |
| `(*uintptr)(c)`|将` copyChecker` 指针转为 `uintptr `指针,适配 CAS 函数的参数类型 |
4.2.3 三步检测逻辑说明
三个if 条件都满足,才会panic。
第一步 :快速校验 (uintptr(*c) != uintptr(unsafe.Pointer(c)))
- 场景1:
c未初始化 →*c是 0,unsafe.Pointer(c)是当前指针地址,0 和当前指针不相等 → 条件为 true,进入下一步; - 场景 2:
c已初始化且未拷贝 →*c等于当前指针地址 ,两者相等→ 条件为 false,整个 if 不成立,直接返回(快速路径); - 场景 3:
c已拷贝 →*c是原地址,unsafe.Pointer(c)是新地址 → 条件为 true,进入下一步
第二步:初始化 CAS (!atomic.CompareAndSwapUintptr(...))
(*uintptr)(c):要操作的内存地址;
0:期望的旧值(未初始化时 *c 是 0);
uintptr(unsafe.Pointer(c)):要设置的新值(当前指针地址)
- 场景 1:
c未初始化 →CAS成功(返回 true)→ !true 是 false → 整个 if 不成立,不 panic(完成初始化); - 场景 2:并发初始化(多个 goroutine 同时调用 check())→ 一个 goroutine CAS 成功,其他失败(返回 false)→ !false 是 true,进入第三步;
- 场景 3:
c已拷贝 →*c不是 0 → CAS 失败(返回 false)→ !false 是 true,进入第三步
第三步:最终校验(uintptr(*c) != uintptr(unsafe.Pointer©))
- 场景 1:并发初始化失败 → 此时
*c已被其他 goroutine 初始化为当前指针地址 → 条件为 false → 不 panic; - 场景 2:
c已拷贝 →*c是原地址,当前指针是新地址 → 条件为 true → 触发 panic("sync.Cond is copied")
4.2.4 设计目的
并发安全 :多个 goroutine 可能同时调用 check(),CAS 保证初始化操作是原子的,避免竞态;
快速路径优化 :第一步能覆盖 99% 的正常场景(已初始化且未拷贝 ),直接返回,不影响性能;
拷贝检测准确性:第三步排除了 "并发初始化竞争" 的干扰,确保只有真正的拷贝才会 panic
4.3 Signal() 方法
go
func (c *Cond) Signal() {
c.checker.check()
runtime_notifyListNotifyOne(&c.notify)
}
作用 : 唤醒等待队列中任意一个阻塞的 goroutine(通常是最先进入队列的那个)。
不强求你一定要持有 c.L 的锁
步骤:
- 拷贝检测: 检测当前 Cond 实例是否被非法拷贝
- 从等待队列中选取一个 goroutine 唤醒 (Go 1.21+ 是
先进先出 FIFO,更早版本是随机)
- 操作对象:
&c.notify,Cond 的等待队列,类型为notifyList,存储所有调用 Wait() 阻塞的 goroutine。 - 唤醒逻辑:
- 被唤醒的 goroutine 会从
runtime_notifyListWait(Wait()方法中阻塞的位置)返回 - 随后该 goroutine 会执行
c.L.Lock()重新获取锁,最终从Wait()方法返回,继续执行后续逻辑 - 若等待队列为空,Signal() 无任何副作用(不会 panic,也不会阻塞),直接返回
- 被唤醒的 goroutine 会从
4.4 Broadcast() 方法
go
func (c *Cond) Broadcast() {
c.checker.check()
runtime_notifyListNotifyAll(&c.notify)
}
作用 : 唤醒等待队列中所有因调用 Cond.Wait() 而阻塞的 goroutine。
不强求你一定要持有 c.L 的锁
步骤:
-
拷贝检测: 检测当前 Cond 实例是否被非法拷贝
- 必要性 :
Cond的notify(等待队列)是和实例绑定的内存地址 ,若Cond被拷贝,拷贝后的实例会指向新的内存地址,而原等待队列仍关联旧实例 ------ 此时调用拷贝实例的Broadcast()会唤醒错误的队列,引发并发安全问题
- 必要性 :
-
广播唤醒所有 goroutine , 遍历等待队列中的所有 goroutine,逐个唤醒(底层按队列 顺序唤醒,FIFO)
- 操作对象:
&c.notify,Cond 的等待队列,类型为notifyList,存储所有调用 Wait() 阻塞的 goroutine。 - 唤醒逻辑:
- 每个被唤醒的 goroutine 会从
runtime_notifyListWait(Wait() 中阻塞的位置)返回 - 随后该 goroutine 会执行
c.L.Lock()重新获取锁,最终从Wait()方法返回,继续执行后续逻辑 - 若等待队列为空,该函数无任何副作用(不会 panic、不会阻塞,直接返回)
- 每个被唤醒的 goroutine 会从
4.5 Wait() 方法
- 把调用者放入
Cond的等待队列中并阻塞,直到被Signal或者Broadcast的方法从等待队列中移除并唤醒。 - 实现 "释放锁→阻塞等待→被唤醒后重新获取锁" 的完整逻辑
- 调用 Wait 方法时必须要持有
c.L的锁,且这个锁必须和保护共享资源(队列)的锁是同一个。
go
func (c *Cond) Wait() {
c.checker.check() // 第一步:检测 Cond 是否被拷贝
t := runtime_notifyListAdd(&c.notify) // 第二步:将当前 goroutine 加入等待队列
c.L.Unlock() // 第三步:释放关联的锁(必须先释放,否则其他 goroutine 无法修改条件)
runtime_notifyListWait(&c.notify, t) // 第四步:阻塞当前 goroutine,等待被唤醒
c.L.Lock() // 第五步:被唤醒后,重新获取锁
}
- 拷贝检测(c.checker.check())
- 调用
copyChecker的check()方法,若 Cond 被拷贝则直接 panic,杜绝非法使用
- 加入等待队列(runtime_notifyListAdd)
runtime_notifyListAdd是 Go 运行时(runtime)的内部函数,作用是将当前 goroutine 加入 Cond 的notify等待队列,并返回一个唯一的队列编号 t
- 释放锁(c.L.Unlock())
- 必须释放锁 :如果不释放,其他 goroutine 无法获取锁来修改条件,也无法调用
Signal()/Broadcast(),会导致死锁 - 关键设计 : 先释放锁,再阻塞,让其他 goroutine 有机会操作共享资源
- 阻塞等待(runtime_notifyListWait)
runtime_notifyListWait是 Go 运行时内部函数,作用:- 挂起当前 goroutine,使其进入休眠状态(不再占用 CPU);
- 直到有其他 goroutine 调用
Signal()/Broadcast(),且该 goroutine 被选中唤醒
- 被唤醒前,goroutine 会一直停在这一步
- 重新获取锁(c.L.Lock())
-
被唤醒后,runtime_notifyListWait 返回,此时立即重新获取关联的锁
-
这一步保证:Wait() 返回后,当前 goroutine 仍然持有锁,可安全地检查 / 修改条件(符合 Cond 的使用规范)
sequenceDiagram
participant G as 当前Goroutine
participant C as Cond
participant L as 关联的锁(Locker)
participant Runtime as Go运行时G->>C: 调用 Wait() G->>C.checker: 执行 check()(检测拷贝) G->>Runtime: 调用 runtime_notifyListAdd(加入等待队列) G->>L: 调用 Unlock()(释放锁) G->>Runtime: 调用 runtime_notifyListWait(阻塞) note over Runtime: 等待 Signal/Broadcast 唤醒 Runtime->>G: 被唤醒,返回 G->>L: 调用 Lock()(重新获取锁) G->>G: Wait() 返回,继续执行

五、使用
5.1 常见错误
-
调用 Wait 的时候没有加锁。
如果调用 Wait 之前不加锁的话,就有可能 Unlock 一个未加锁的 Locker,所以,调用
cond.Wait方法之前一定要加锁 -
只调用了一次 Wait,没有检查等待条件是否满足
waiter goroutine 被唤醒不等于等待条件被满足
5.2 示例
go
package main
import (
"log"
"math/rand"
"sync"
"time"
)
func main() {
c := sync.NewCond(&sync.Mutex{})
var ready int
for i := 0; i < 10; i++ {
go func(i int) {
time.Sleep(time.Duration(rand.Int63n(10)) * time.Second)
// 加锁更改等待条件
c.L.Lock()
ready++
c.L.Unlock()
log.Printf("运动员#%d 已准备就绪\n", i)
// 广播唤醒所有的等待者
c.Broadcast()
}(i)
}
c.L.Lock()
//
for ready != 10 {
c.Wait()
log.Println("裁判员被唤醒一次")
}
c.L.Unlock()
//所有的运动员是否就绪
log.Println("所有运动员都准备就绪。比赛开始,3,2,1, ......")
}
Cond.Wait() 的核心语义是:"释放锁并等待,直到被唤醒后重新获取锁" ------ 但它不保证 "被唤醒时条件一定满足" 。
根本原因 :操作系统层面的虚假唤醒,导致 Wait() 可能无理由返回;
业务原因 :唤醒后抢锁的过程中,条件可能被其他 goroutine 抢占修改;
解决方案:必须用 for 循环替代 if 检查条件,唤醒后重新验证,不满足则继续等待
5.3 特性
- Cond 和一个
Locker关联,可以利用这个 Locker 对相关的依赖条件更改提供保护 - Cond 可以同时支持
Signal 和Broadcast方法,而 Channel 只能同时支持其中一种 - Cond 的 Broadcast 方法可以被
重复调用。等待条件再次变成不满足的状态后,我们又可以调用 Broadcast 再次唤醒等待的 goroutine。这也是 Channel 不能支持的,Channel 被 close 掉了之后不支持再 open
5.4 Cond 实现容量有限的 queue
go
package main
import (
"log"
"sync"
)
// BoundedQueue 容量有限的并发安全队列
type BoundedQueue struct {
mu sync.Mutex // 保护队列的互斥锁
condNotEmpty *sync.Cond // 队列不满的条件变量(入队等待)
condNotFull *sync.Cond // 队列不空的条件变量(出队等待)
queue []interface{} // 存储元素的切片
capacity int // 队列最大容量
}
// NewBoundedQueue 创建指定容量的有限队列
func NewBoundedQueue(capacity int) *BoundedQueue {
bq := &BoundedQueue{
capacity: capacity,
queue: make([]interface{}, 0, capacity),
}
// 关键修复:两个 Cond 都关联队列的 mu 锁(而非新锁)
bq.condNotEmpty = sync.NewCond(&bq.mu)
bq.condNotFull = sync.NewCond(&bq.mu)
return bq
}
// Enqueue 入队:队列满则阻塞,直到有空间
func (q *BoundedQueue) Enqueue(v interface{}) {
q.mu.Lock()
defer q.mu.Unlock()
// 循环检查:队列满则等待(防止虚假唤醒)
for len(q.queue) == q.capacity {
log.Println("队列已满,入队阻塞等待...")
q.condNotFull.Wait()
}
// 入队
q.queue = append(q.queue, v)
log.Println("入队成功:", v)
// 唤醒等待出队的 goroutine(队列从空→非空时需要)
q.condNotEmpty.Signal()
}
// Dequeue 出队:队列空则阻塞,直到有元素
func (q *BoundedQueue) Dequeue() interface{} {
q.mu.Lock()
defer q.mu.Unlock()
// 循环检查::队列空则等待(防止虚假唤醒)
for len(q.queue) == 0 {
log.Println("队列已空,出队阻塞等待...")
q.condNotEmpty.Wait()
}
// 出队
v := q.queue[0]
log.Println("出队成功:", v)
q.queue = q.queue[1:]
// 唤醒等待入队的 goroutine(队列从满→非满时需要)
q.condNotFull.Signal()
return v
}
// Len 返回当前队列长度(仅用于演示,实际使用需加锁)
func (q *BoundedQueue) Len() int {
q.mu.Lock()
defer q.mu.Unlock()
return len(q.queue)
}
func main() {
queue := NewBoundedQueue(3)
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(i int) {
defer wg.Done()
queue.Enqueue(i)
}(i)
}
wg.Add(5)
for i := 0; i < 5; i++ {
go func(i int) {
defer wg.Done()
item := queue.Dequeue()
log.Printf("消费者%d 消费元素:%v", i, item)
}(i)
}
wg.Wait()
log.Printf("所有操作完成,最终队列长度:%d", queue.Len())
}