Rust in Everything
基于 crossbeam-utils 0.8.23

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 是它的另一半。

类型SendSyncClone做什么
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 分三步:

  1. compare_exchange(NOTIFIED, EMPTY) 成功说明之前有人 unpark 过,消费掉令牌直接返回,不碰锁。
  2. 截止时间已过就返回。
  3. 拿锁,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 的写同步。

📝NOTE
冷知识:同一段代码至少有三份

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::parkcrossbeam_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 起计数是原子变量,锁只在归零和等待时用。

💬 关联讨论

发起讨论
加载中...