Once
主线位置:主线 1 的一个变体。
Once的慢路径就是RawMutex::lock_slow的同一个骨架(抢LOCKED_BIT→ 没人停车才自旋 → 置PARKED_BIT→park),区别在于拿到锁的那个线程执行完闭包后不是「解锁」,而是一次性写成DONE_BIT并unpark_all。
stateDiagram-v2 direction LR New: 0 新建 Run: LOCKED 闭包执行中 RunP: LOCKED 加 PARKED 有人在等 Done: DONE Poison: POISON New --> Run: CAS 抢到 LOCKED_BIT Poison --> Run: call_once_force 抢到,同时清 POISON Run --> RunP: 等待者置 PARKED_BIT 后停车 Run --> Done: 闭包返回,swap(DONE_BIT) RunP --> Done: 闭包返回,swap 后 unpark_all Run --> Poison: 闭包 panic,PanicGuard swap(POISON_BIT) RunP --> Poison: 闭包 panic,swap 后 unpark_all
1 字节,4 位
const DONE_BIT: u8 = 1;
const POISON_BIT: u8 = 2;
const LOCKED_BIT: u8 = 4;
const PARKED_BIT: u8 = 8;
pub struct Once(AtomicU8);(77 行)。state() 把四位翻译成 OnceState 的四个值:Done 优先,其次 InProgress(LOCKED_BIT),再次 Poisoned,都没有就是 New(86-99 行)。注意 LOCKED_BIT 和 POISON_BIT 可以同时出现:call_once_force 在一个已毒化的 Once 上重新执行时(见下)。
文档列的与 std 的差异(58-64 行):1 字节而不是 1 个字;不要求 'static;「Relaxed memory barriers in the fast path」;自适应自旋。第三条和 0.12.5 的代码对不上:快路径是一次 load(Ordering::Acquire)(下一节),不是 Relaxed。
快路径:一次读
#[inline]
pub fn call_once<F>(&self, f: F)
where
F: FnOnce(),
{
if self.0.load(Ordering::Acquire) == DONE_BIT {
return;
}
let mut f = Some(f);
self.call_once_slow(false, &mut |_| unsafe { f.take().unchecked_unwrap()() });
}
初始化完成之后,每次调用就是一次 Acquire 读加一次比较。Acquire 与执行闭包那个线程最后的 swap(DONE_BIT, Release) 配对,保证读到 DONE_BIT 的线程能看到闭包里的写入(文档里的「happens-before relation between the closure and code executing after the return」)。比较用的是 == DONE_BIT 而不是 & DONE_BIT:完成之后状态字恰好是 DONE_BIT,其余位全是 0(见下面的 swap)。
FnOnce 被包进 Option 再以 &mut dyn FnMut(OnceState) 传给非泛型的 call_once_slow:注释说这样慢路径只编译一份,而且没有办法不分配地经由虚调用调用一个 FnOnce(187-197 行)。
慢路径
call_once_slow 的循环和 RawMutex::lock_slow 几乎逐行对应,只多了开头两个判断:
// If another thread called the closure, we're done
if state & DONE_BIT != 0 {
// An acquire fence is needed here since we didn't load the
// state with Ordering::Acquire.
fence(Ordering::Acquire);
return;
}
// If the state has been poisoned and we aren't forcing, then panic
if state & POISON_BIT != 0 && !ignore_poison {
// Need the fence here as well for the same reason
fence(Ordering::Acquire);
panic!("Once instance has previously been poisoned");
}
// Grab the lock if it isn't locked, even if there is a queue on it.
// We also clear the poison bit since we are going to try running
// the closure again.
if state & LOCKED_BIT == 0 {
match self.0.compare_exchange_weak(
state,
(state | LOCKED_BIT) & !POISON_BIT,
Ordering::Acquire,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(x) => state = x,
}
continue;
}
// ... spin if no PARKED_BIT, set PARKED_BIT, park(addr, validate = state == LOCKED | PARKED)
慢路径里的读都是 Relaxed,看到 DONE_BIT 或 POISON_BIT 时补一个 Acquire 栅栏,效果等同于用 Acquire 读。抢到 LOCKED_BIT 时顺手清掉 POISON_BIT:能走到这里且带着 POISON_BIT 的,只可能是 call_once_force。
停车的 timed_out 回调是 unreachable!():Once 从不带超时停车。醒来后不看令牌,回到循环开头重新读状态:被叫醒只意味着「执行者结束了」,结局是 DONE 还是 POISON 要自己看。
执行闭包:PanicGuard
struct PanicGuard<'a>(&'a Once);
impl<'a> Drop for PanicGuard<'a> {
fn drop(&mut self) {
// Mark the state as poisoned, unlock it and unpark all threads.
let once = self.0;
let state = once.0.swap(POISON_BIT, Ordering::Release);
if state & PARKED_BIT != 0 {
let addr = once as *const _ as usize;
unsafe {
parking_lot_core::unpark_all(addr, DEFAULT_UNPARK_TOKEN);
}
}
}
}
let guard = PanicGuard(self);
let once_state = if state & POISON_BIT != 0 {
OnceState::Poisoned
} else {
OnceState::New
};
f(once_state);
mem::forget(guard);
// Now unlock the state, set the done bit and unpark all threads
let state = self.0.swap(DONE_BIT, Ordering::Release);
if state & PARKED_BIT != 0 {
let addr = self as *const _ as usize;
unsafe {
parking_lot_core::unpark_all(addr, DEFAULT_UNPARK_TOKEN);
}
}
- 成功:
swap(DONE_BIT)一次把LOCKED_BIT、PARKED_BIT都清掉,只留DONE_BIT;旧值里有PARKED_BIT就unpark_all。 - 闭包 panic:unwind 时
PanicGuard::drop把状态swap成POISON_BIT,同样叫醒所有等待者。等待者醒来看到POISON_BIT:call_once的等待者跟着 panic,call_once_force的等待者会去抢LOCKED_BIT再试一次。 - 闭包传进来的
OnceState只可能是New或Poisoned(快照取自抢锁那一刻)。
和 Mutex 不同,Once 用 unpark_all 一次叫醒全部等待者:它们醒来后只读一下状态就走,不需要互斥。
与 std 的对照
std 的 Once 在 macOS 上是 sys/sync/once/queue.rs(等待节点链表,8 字节),在 Linux 等平台上是 futex.rs(4 字节)(once/mod.rs:10-36)。std 的毒化报错和 parking_lot 一字不差(once/queue.rs:167)。OnceLock 在内部用的是 call_once_force(sync/once_lock.rs:562-580),所以初始化闭包 panic 不会让它卡住:下一次 get_or_init 会再试。LazyLock 则会毒化(sync/lazy_lock.rs:415)。
parking_lot Once | std Once | std OnceLock<u64> | std LazyLock<u64> | |
|---|---|---|---|---|
| 本机大小 | 1 | 8 | 16 | 16 |
| Linux 大小(编译期断言) | 1 | 4 | 16 | — |
| 初始化 panic 后再调用 | panic「Once instance has previously been poisoned」 | 同左 | 重新执行初始化 | panic「LazyLock instance has previously been poisoned」 |
| 绕过毒化 | call_once_force | call_once_force | 不需要 | 无 |
实验 E10
方法(exp-parking_lot/once,不开 deadlock_detection):
- ① 快路径:初始化完成后循环 2 亿次
call_once(stdOnce::call_once、OnceLock::get同样),外加一个空循环作基准;程序内部 5 轮取中位数,整个程序跑 3 次。 - ② 并发:12 个线程用
Barrier同时对一个新Once调call_once,闭包里计数;2000 轮,每轮计数必须恰好是 1。OnceLock::get_or_init同样做一遍。反向验证:同一轮里再做一次不加锁的「先读标志、再计数、再写标志」,它应当出现计数大于 1 的轮次。 - ③ 毒化:闭包 panic 后再调
call_once、call_once_force,与 stdOnce、OnceLock、LazyLock对照。 - ④ 等待中毒化:一个线程在闭包里睡 100 ms 后 panic,期间另外 4 个线程在同一个
Once(或OnceLock)上等。
结果:
① 快路径(22:37,负载 2.67 / 2.98;3 次运行的中位数,每次运行本身是 5 轮的中位数;括号是 3 次的范围):
| ns / 次 | |
|---|---|
parking_lot Once::call_once(已完成) | 0.346(0.344–0.354) |
std Once::call_once(已完成) | 0.344(0.340–0.346) |
std OnceLock::get | 0.422(0.420–0.437) |
空循环(只有 black_box(i)) | 0.300(0.298–0.300) |
三者都只比空循环多 0.04–0.12 ns:这是紧循环里的吞吐,读操作和循环本身重叠执行,量到的不是单次延迟。能说的只是「同量级、都接近免费」,parking_lot 的 Acquire 读与 std 没有可测的差别。
② 2000 轮 × 12 线程:parking_lot Once 和 OnceLock 计数不为 1 的轮次都是 0;不加锁的那一组有 104 轮计数大于 1,说明这个测法能抓到重复执行。
③ 毒化(原文):
pl first call_once: Some("init failed"); state now Poisoned
pl second call_once: Some("Once instance has previously been poisoned"); state now Poisoned
pl call_once_force closure sees Poisoned (poisoned() = true)
pl after call_once_force: state Done
std Once first: Some("init failed"); is_completed false
std Once second: Some("Once instance has previously been poisoned")
std call_once_force closure sees is_poisoned() = true
OnceLock first get_or_init: Some("init failed"); get() = None
OnceLock second get_or_init: Ok(7)
LazyLock first deref: Some("init failed")
LazyLock second deref: Some("LazyLock instance has previously been poisoned")
call_once_force 是反向验证:它看到 Poisoned,执行成功后状态变成 Done。
④ 等待中毒化:parking_lot 的 4 个等待者全部 panic「Once instance has previously been poisoned」——它们被 PanicGuard 叫醒后看到 POISON_BIT。OnceLock 的 4 个等待者里有一个重新执行了初始化(计数 1),4 个都拿到了同一个值。
结论:在需要「初始化失败可以重试」的地方,OnceLock 的语义和 parking_lot Once 不同,迁移时要注意(坑 once-poisoned)。OnceLock、LazyLock 稳定以后,新代码里需要 parking_lot Once 的场合不多:它的优势只剩 1 字节和不要求 'static(std 的 Once::new 同样是 const fn,也不要求 'static,这一条在今天已经不算差别)。
小结
Once= 一个AtomicU8的四位:DONE、POISON、LOCKED、PARKED。完成后状态恰好是DONE_BIT,快路径是一次Acquire读加==比较(文档说是Relaxed,与代码不符)。- 慢路径和
RawMutex::lock_slow同一个骨架:抢LOCKED_BIT(call_once_force同时清POISON_BIT)、没人停车才自旋、置PARKED_BIT后停车;醒来重读状态。 - 执行者成功时
swap(DONE_BIT),panic 时PanicGuardswap(POISON_BIT),两种情况都unpark_all。 - 毒化后
call_oncepanic,等待中的线程也一起 panic;call_once_force能看到Poisoned并重试。stdOnce、LazyLock也毒化,OnceLock不毒化、下次重试。 - E10(本机):初始化后的
call_once与 std 同为约 0.35 ns / 次(紧循环吞吐);12 线程并发 2000 轮都只执行一次;毒化语义与 stdOnce相同、与OnceLock不同。