Rust in Everything
基于 parking_lot 0.12.5

Once instance has previously been poisoned

主线位置:主线 1 的骨架用在 Once 上。执行初始化闭包的线程持有 LOCKED_BIT;闭包 panic 时 PanicGuard::drop 把状态换成 POISON_BIT 并 unpark_all,所有等待者醒来看到毒化(Once)。

sequenceDiagram
  participant I as 初始化线程
  participant O as Once 状态
  participant W as 4 个等待者
  I->>O: 抢到 LOCKED_BIT,执行闭包
  W->>O: call_once:LOCKED,停车
  I->>I: 闭包 panic
  I->>O: PanicGuard:swap(POISON_BIT),unpark_all
  W->>O: 醒来读到 POISON_BIT
  W->>W: 全部 panic「Once instance has previously been poisoned」
  Note over O: 之后每次 call_once 都 panic,call_once_force 才能重试

报错原文

E10 ③(exp-parking_lot/once,不开 deadlock_detection),闭包 panic 后再调用:

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")

E10 ④,等待中毒化:一个线程在闭包里睡 100 ms 后 panic,期间另外 4 个线程在同一个 Once 上等。parking_lot 的 4 个等待者全部 panic,报错同上;OnceLock 的 4 个等待者里有一个重新执行了初始化,4 个都拿到了同一个值。

一次初始化失败,可能连带四个线程 panic。

源码出处

once.rs:211-216:

// 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");
}

毒化发生在 PanicGuard::drop:swap(POISON_BIT, Release),旧值有 PARKED_BIT 就 unpark_all。等待者醒来不看令牌,回到循环开头读状态,读到 POISON_BIT 就走上面那个分支。

std 的 Once 用的是同一句话(once/queue.rs:167);OnceLock 内部用 call_once_force,所以不会卡在毒化上(sync/once_lock.rs:562-580)。

这是 parking_lot 里唯一有毒化的原语,Mutex / RwLock 都没有(持锁时 panic)。

怎么改

  • 初始化可能失败、希望下次重试:改用 std OnceLock::get_or_init:闭包 panic 后不毒化,下一次调用重新执行初始化。OnceLock 稳定以后,新代码里需要 parking_lot Once 的理由只剩 1 字节的大小。
  • 要继续用 Once:用 call_once_force(|state| ..),闭包里 state.poisoned() 为真时做清理再重试。等待者也要用 call_once_force,否则照样 panic。
  • 初始化闭包里别 panic:把可能失败的部分(读文件、解析配置)放到闭包外,失败时返回错误,闭包里只做不会失败的赋值。

小结

  • 初始化闭包 panic 后,parking_lot Once 毒化,之后每次 call_once 都 panic「Once instance has previously been poisoned」,与 std Once 一字不差。
  • 正在等的线程也一起 panic:E10 中 4 个等待者全部 panic。
  • std OnceLock 不毒化、下次重试;LazyLock 毒化。
  • 要重试就用 OnceLock 或 call_once_force。

💬 关联讨论

发起讨论
加载中...