notify_all() 返回 32,可是一个线程也没醒
主线位置:主线 2 全部。
notify_all→unpark_requeue:mutex 被锁着就RequeueAll,把所有等待者挪到 mutex 的队列、一个都不叫醒;返回值是unparked + requeued(一次 notify_all)。
flowchart TB n["持锁调用 cv.notify_all()"] --> rq["unpark_requeue:mutex 被锁,RequeueAll"] rq --> cnt["unparked = 0,requeued = 32"] cnt --> ret["返回 32"] rq --> q["32 个线程改 key,接到 mutex 队列尾"] q --> rel["通知者放锁:unlock_slow 只叫醒 1 个"] rel --> chain["之后每次 unlock 叫醒下一个"]
现象
两种用法会出错:
- 把
notify_all()的返回值当成「已经醒来、正在干活的线程数」,用来做统计、限流或者等待它们全部回报。 - 把
notify_one()返回true当成「有一个线程马上会运行」,接着在仍持有锁的情况下等它的回应,结果谁也动不了。
实测
E6(exp-parking_lot/condvar-herd,不开 deadlock_detection):N 个线程在同一个 condvar 上等,主线程持锁或放锁后 notify_all,N 取 1、4、16、64,每个配置内部重复 11 次,整组 3 遍:
notify_all的返回值在所有配置、所有重复里都等于 N。持锁调用(RequeueAll:0 个叫醒 + N 个挪走)和放锁后调用(1 个叫醒 + N − 1 个挪走)没有区别。- 中间对照:持锁循环 N 次
notify_one,每次都返回true,而每次都只是RequeueOne,一个线程也没叫醒。 - 被挪走的线程是一个接一个拿到锁的:N = 64 时相邻两个持锁者的交接间隔中位数约 10 µs,总时间约 2.0 ms,由每个线程持锁 20 µs 串行决定(完整表见 Condvar)。
源码出处
文档与代码不一致:
| 文档 | 代码 | |
|---|---|---|
notify_all | 「Returns the number of threads woken up.」(condvar.rs:179) | res.unparked_threads + res.requeued_threads(notify_all_slow) |
notify_one | 「Returns whether a thread was woken up.」(condvar.rs:106) | res.unparked_threads + res.requeued_threads != 0;mutex 被锁时走 RequeueOne(notify_one_slow) |
validate 里调 mark_parked_if_locked:mutex 正被锁着就给它补 PARKED_BIT 并选 RequeueAll / RequeueOne,否则选 UnparkOneRequeueRest / UnparkOne。所以「叫醒了几个」取决于调用时 mutex 是否被锁,而返回值不区分这两种情况。
std 的 Condvar::notify_all / notify_one 都返回 (),没有这个问题,也没有这个信息。
怎么改
- 只把返回值当作「有没有等待者」:
notify_one() == false可靠地表示当时没人在等(文档例子「Nobody was listening for this」就是这么用的);notify_all() == 0同理。 - 需要「醒了几个」「处理完了几个」,让被唤醒的线程自己在锁里计数,或者用 channel 回报。
- 不要持锁等被通知者的回应:被通知的线程要先拿到这把锁才能从
wait返回。先放锁,再等(另一个条件变量或 channel)。 - 惊群本身在本机并不严重(E6:std 的上下文切换只比 parking_lot 多 5%–15%),requeue 主要让第一个等待者更早拿到锁(N = 64 时约 19–27 µs 对 103–132 µs)。
小结
notify_all返回被叫醒加被挪走的线程数,持锁调用时真正叫醒的是 0 个;notify_one的true也可能只是挪了一个。- 文档写「woken up」,代码是「受影响」,以代码为准。
- E6:返回值在所有配置下都等于等待者数 N。
- 返回值只用来判断「有没有人在等」;要计数让被唤醒者自己报。
💬 关联讨论
发起讨论加载中...