Rust in Everything
基于 parking_lot 0.12.5

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。
  • 返回值只用来判断「有没有人在等」;要计数让被唤醒者自己报。

💬 关联讨论

发起讨论
加载中...