Parker 与 WaitGroup
主线位置:都不在三条主线上。channel 阻塞时用的是
std::thread::park,不是Parker;WaitGroup的唯一内部用户是 thread::scope。放在这里,是因为它们和 tokio、std 里的同类实现几乎逐行相同,值得对照着看一次。
flowchart LR E[EMPTY] -->|"park:CAS EMPTY→PARKED,持锁"| P[PARKED] P -->|"unpark:swap NOTIFIED,见 PARKED 就 notify_one"| N[NOTIFIED] E -->|"unpark:swap NOTIFIED"| N N -->|"park:CAS NOTIFIED→EMPTY,立即返回"| E
Parker:一个令牌
Parker 和 std::thread::park 的语义一样:unpark 给一个「令牌」,park 消费令牌;令牌最多一个,连着 unpark 十次,后面的 park 也只会少睡一次。区别在于它和线程无关:Parker 是一个普通值,Unparker 是它的另一半。
| 类型 | Send | Sync | Clone | 做什么 |
|---|---|---|---|---|
Parker | 是 | 否(PhantomData<*const ()>) | 否 | park、park_timeout、park_deadline;unparker() 借出另一半 |
Unparker | 是 | 是 | 是 | unpark |
两者共享一个 Arc<Inner>,parker.rs:310:
const EMPTY: usize = 0;
const PARKED: usize = 1;
const NOTIFIED: usize = 2;
struct Inner {
state: AtomicUsize,
lock: Mutex<()>,
cvar: Condvar,
}
park 分三步:
compare_exchange(NOTIFIED, EMPTY)成功说明之前有人unpark过,消费掉令牌直接返回,不碰锁。- 截止时间已过就返回。
- 拿锁,
compare_exchange(EMPTY, PARKED);如果这时发现是NOTIFIED(刚好有人unpark),用swap(EMPTY)消费掉返回。之后在Condvar上等,醒来再试NOTIFIED → EMPTY,不成就当作虚假唤醒继续等。
unpark 一律 swap(NOTIFIED)。原值是 EMPTY 或 NOTIFIED 就返回;是 PARKED 才去叫醒,叫醒之前先拿一下锁再放掉:
// There is a period between when the parked thread sets `state` to `PARKED` (or last
// checked `state` in the case of a spurious wakeup) and when it actually waits on `cvar`.
// If we were to notify during this period it would be ignored and then when the parked
// thread went to sleep it would never wake up. Fortunately, it has `lock` locked at this
// stage so we can acquire `lock` to wait until it is ready to receive the notification.
//
// Releasing `lock` before the call to `notify_one` means that when the parked thread wakes
// it doesn't get woken only to have to wait for us to release `lock`.
drop(self.lock.lock().unwrap());
self.cvar.notify_one();
park 是持锁做 EMPTY → PARKED 的,直到 cvar.wait 才放锁。unpark 能拿到锁,就说明对方已经在 wait 里了,notify_one 不会落空。
内存序上,所有原子操作都是 SeqCst。注释特别解释了两处为什么必须是「写」:unpark 用 swap 而不用「读到 NOTIFIED 就返回」的 CAS,是因为要做一次 release 写,让被唤醒的线程能看到 unpark 之前的写;park 的第 3 步里明知是 NOTIFIED 也要 swap 一次,是为了和最近那次 unpark 的写同步。
std 在用 pthread 的平台上实现 thread::park 的 thread_parking/pthread.rs 也是这套三态,「先拿锁再 notify_one」那段注释几乎一字不差(macOS 上 std 改用 darwin.rs 的系统调用,不走这份)。tokio 的 ParkThread 是几乎逐行相同的又一份,tokio 卷的 driver 与 park 有完整的对照和一个「值班室的门」的比方。三份的区别只在外壳:std 的挂在每个线程上,tokio 的绑在线程本地变量上,crossbeam 的是一个独立的值。
和 std::thread::park 的区别
std::thread::park | crossbeam_utils::sync::Parker | |
|---|---|---|
| 令牌属于谁 | 每个线程一个,Thread::unpark | 每个 Parker 一个,和线程无关 |
| 谁能 park | 只能是当前线程 | 持有 Parker 的那个线程(!Sync 保证只有一个线程能调 park) |
| 跨 FFI | 不行 | Parker::into_raw / Unparker::into_raw 把 Arc<Inner> 变成 *const (),from_raw 还原 |
| 超时 | park_timeout(Duration) | park_timeout(Duration)、park_deadline(Instant);Duration 加到 Instant 溢出时退化成不限时的 park |
crossbeam 自己的几个 crate 都不用 Parker:channel 的 Context::wait_until 直接调 thread::park / park_timeout,Waker 存的是 Thread 句柄(Context 与 Waker)。
WaitGroup:数到零
WaitGroup 是一个计数器:new 时为 1,clone 加 1,drop 减 1,wait 也减 1 并等到计数归零。它和 Go 的 sync.WaitGroup 名字一样,用法不一样:不用 Add(n),每个参与者拿一份克隆,做完 drop 掉,主线程最后 wait。
let wg = WaitGroup::new();
for _ in 0..4 {
let wg = wg.clone();
thread::spawn(move || {
// ... 做事
drop(wg);
});
}
wg.wait(); // 主线程这一份也算一个
Inner 是 Condvar、Mutex<()> 和一个 AtomicUsize。wait:
pub fn wait(self) {
// SAFETY: this is equivalent to let Self { inner } = self, without calling our Drop.
let inner = unsafe {
let slf = ManuallyDrop::new(self);
core::ptr::read(&slf.inner)
};
if inner.count.fetch_sub(1, Ordering::AcqRel) == 1 {
// Acquire lock after updating count, see below.
drop(inner.lock.lock().unwrap());
inner.cvar.notify_all();
return;
}
// We check the counter while holding the lock, and notifiers acquire
// the lock between updating the counter and notifying, ensuring we
// can not miss the notification.
let mut guard = inner.lock.lock().unwrap();
while inner.count.load(Ordering::Acquire) != 0 {
guard = inner.cvar.wait(guard).unwrap();
}
}
和 Parker 同一个技巧:减到零的一方「先拿一下锁再放掉,然后 notify_all」,等待方「持锁检查计数、再 wait」,两边在锁上排队,通知不会丢。wait 消费 self,用 ManuallyDrop 把 Arc 取出来,避免 Drop 再减一次。Drop 只用 Release 减;clone 用 Relaxed 加(和 Arc::clone 同理:能克隆说明手里已经有一份,计数不会从 0 起跳)。
这是 0.8.22 的写法。之前(#1195,4cb6cbc9,2026-02)计数是 Mutex<usize>,每次 clone 和 drop 都要拿锁;现在只有减到零的那一次和 wait 碰锁。
小结
Parker是AtomicUsize三态(EMPTY / PARKED / NOTIFIED)加Mutex<()>和Condvar,全部SeqCst;unpark先拿一下锁再notify_one,保证不丢唤醒。算法和 std、tokio 的 park 实现相同。Parker是Send不是Sync,Unparker可克隆、可跨线程;可以into_raw/from_raw过 FFI。crossbeam 自己的 channel 不用它,用的是thread::park。WaitGroup计数从 1 开始,clone加、drop减,wait消费自身并等到零;0.8.22 起计数是原子变量,锁只在归零和等待时用。