Go Cond 源码解析

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 的锁

步骤

  1. 拷贝检测: 检测当前 Cond 实例是否被非法拷贝
  2. 从等待队列中选取一个 goroutine 唤醒 (Go 1.21+ 是先进先出 FIFO,更早版本是随机
  • 操作对象:&c.notify ,Cond 的等待队列,类型为 notifyList,存储所有调用 Wait() 阻塞的 goroutine。
  • 唤醒逻辑:
    • 被唤醒的 goroutine 会从 runtime_notifyListWaitWait() 方法中阻塞的位置)返回
    • 随后该 goroutine 会执行c.L.Lock()重新获取锁,最终从 Wait() 方法返回,继续执行后续逻辑
    • 若等待队列为空,Signal() 无任何副作用(不会 panic,也不会阻塞),直接返回

4.4 Broadcast() 方法

go 复制代码
func (c *Cond) Broadcast() {
	c.checker.check()
	runtime_notifyListNotifyAll(&c.notify)
}

作用 : 唤醒等待队列中所有因调用 Cond.Wait() 而阻塞的 goroutine

不强求你一定要持有 c.L 的锁

步骤

  1. 拷贝检测: 检测当前 Cond 实例是否被非法拷贝

    • 必要性Cond notify(等待队列)是和实例绑定的内存地址 ,若 Cond 被拷贝,拷贝后的实例会指向新的内存地址,而原等待队列仍关联旧实例 ------ 此时调用拷贝实例的 Broadcast() 会唤醒错误的队列,引发并发安全问题
  2. 广播唤醒所有 goroutine , 遍历等待队列中的所有 goroutine,逐个唤醒(底层按队列 顺序唤醒,FIFO

  • 操作对象:&c.notify ,Cond 的等待队列,类型为 notifyList,存储所有调用 Wait() 阻塞的 goroutine。
  • 唤醒逻辑:
    • 每个被唤醒的 goroutine 会从 runtime_notifyListWait(Wait() 中阻塞的位置)返回
    • 随后该 goroutine 会执行c.L.Lock()重新获取锁,最终从 Wait() 方法返回,继续执行后续逻辑
    • 若等待队列为空,该函数无任何副作用(不会 panic、不会阻塞,直接返回)

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() // 第五步:被唤醒后,重新获取锁
}
  1. 拷贝检测(c.checker.check())
  • 调用 copyCheckercheck() 方法,若 Cond 被拷贝则直接 panic,杜绝非法使用
  1. 加入等待队列(runtime_notifyListAdd)
  • runtime_notifyListAdd 是 Go 运行时(runtime)的内部函数,作用是将当前 goroutine 加入 Cond 的 notify 等待队列,并返回一个唯一队列编号 t
  1. 释放锁(c.L.Unlock())
  • 必须释放锁 :如果不释放,其他 goroutine 无法获取锁来修改条件,也无法调用 Signal()/Broadcast(),会导致死锁
  • 关键设计先释放锁,再阻塞,让其他 goroutine 有机会操作共享资源
  1. 阻塞等待(runtime_notifyListWait)
  • runtime_notifyListWait 是 Go 运行时内部函数,作用:
    • 挂起当前 goroutine,使其进入休眠状态(不再占用 CPU);
    • 直到有其他 goroutine 调用 Signal()/Broadcast(),且该 goroutine 被选中唤醒
  • 被唤醒前,goroutine 会一直停在这一步
  1. 重新获取锁(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 常见错误

  1. 调用 Wait 的时候没有加锁。

    如果调用 Wait 之前不加锁的话,就有可能 Unlock 一个未加锁的 Locker,所以,调用 cond.Wait 方法之前一定要加锁

  2. 只调用了一次 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())
}
相关推荐
F1FJJ2 小时前
我用一条命令把内网的 RDP 桌面开到了浏览器里 —— Shield CLI 与主流隧道工具的技术对比
网络·golang
lars_lhuan4 小时前
Go map 与并发
后端·golang
Lewiis4 小时前
Go语言的错误处理机制
开发语言·后端·golang
benzun_yinzi6 小时前
go升级之后找不到goroot解决办法
golang
lars_lhuan7 小时前
Go Once
开发语言·golang
hongtianzai7 小时前
Go vs Java:终极性能对决
java·开发语言·golang
贺小涛7 小时前
Golang Gin框架核心原理与架构解析
架构·golang·gin
古城小栈1 天前
Go 底层代码的完整分类
开发语言·后端·golang
耳冉鹅1 天前
Go无锁共享内存环形缓冲区设计
开发语言·golang