发布于2026-07-12 阅读(0)
扫一扫,手机访问
Condition 变量的使用有几个关键原则:cond.Wait 必须在 while 循环中调用,因为虚假唤醒和竞态要求反复检查谓词;cond.Signal 须在持锁修改状态后、释放锁前调用;cond.Wait 与 cond.Signal 必须共用同一把锁;Condition 适用于低频、长时间等待共享状态变更的场景。下面逐一展开。
直接用 if 判断后调用 cond.wait() 是最容易踩的坑之一。问题在于,操作系统和运行时环境都允许线程在没有任何信号的情况下被唤醒——这被称为虚假唤醒(spurious wakeup)。另外,即使只有一个线程被 signal 唤醒,也可能出现多个线程同时醒来的情况(多核系统下尤为常见)。如果只用 if 检查一次,唤醒后就直接往下执行,很可能条件已经不成立,数据错乱就在所难免。

正确的做法是:拿到锁之后,用 while 循环反复检查谓词(predicate),只有当条件真正满足时才跳出循环。以 Go 为例:
lock.Lock()
defer lock.Unlock()
for len(queue) == 0 { // 谓词:队列为空
cond.Wait() // 自动释放 lock,唤醒后自动重新获取
}
item := queue[0]
queue = queue[1:]
if len(queue) == 0 { cond.Wait() }——那相当于赌运气。await()、Go 的 Cond.Wait()、C 的 pthread_cond_wait(),无一例外。cond.Signal() 本身不做数据操作,也不释放锁,它的作用只是向等待队列发一个“醒醒”通知。如果先释放锁再调用 signal,就会引入一个极难排查的竞态:新线程可能在你释放锁之后、signal 之前抢先加锁并再次阻塞,等你的 signal 发出时,已经没有等待者了——通知丢失,等待线程永远醒不来。
安全的顺序是:
lock.Lock()
// 修改共享状态:如 queue.push(item), count++
if len(queue) == 1 { // 刚从空变为非空,值得通知消费者
cond.Signal()
}
lock.Unlock()
cond.Signal(),哪怕你觉得“逻辑上更顺”。cond.Broadcast() 或 cond.SignalAll(),但要注意性能开销更高。Condition 不是一个独立的同步原语,它必须与且仅与一个互斥锁绑定。Go 的 sync.Cond{L: &mutex}、Ja va 的 ReentrantLock.newCondition()、C 的 pthread_cond_wait(&cond, &mutex) 都强制要求传入 mutex。跨锁使用会引发未定义行为——轻则死锁,重则内存破坏。
常见的误用案例:
mutexA 加锁,却传 mutexB 给 cond.Wait() → 直接崩溃或静默失败。cond 被多个不同锁的临界区复用 → 状态混乱,唤醒完全失效。cond.Wait() 返回后已经自动重新加锁,手动多加一次导致死锁。Condition 最适合“等待某个共享内存状态改变”的场景,尤其是当状态由多个线程协同维护、通知频率低、等待时间长的时候。相比轮询 + time.Sleep(),它能省下大量 CPU;相比 channel,它更贴近底层共享变量模型(比如 ring buffer、引用计数、文件描述符就绪等)。
但也有些铁律需要记住:
sync.Once 或 sync.WaitGroup 更轻量。最后一点最容易忽略:Condition 的等待队列不保证 FIFO 顺序,signal 唤醒哪个线程完全取决于调度器和底层实现。永远不要依赖唤醒顺序来做业务逻辑判断——那不是它承诺的功能。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8