Rust in Everything
基于 lock_api 0.4.14

GuardSend 与 send_guard

主线位置:主线 1 的出口。守卫析构时调用 raw.unlock(),这一章回答「这次 unlock 能不能发生在另一个线程上」:lock_api 用一个标记类型把答案编进守卫的类型里,parking_lot 用一个 Cargo feature 选择标记。

flowchart TB
  feat{"parking_lot 开了 send_guard?"}
  feat -- "否(默认)" --> nosend["crate::GuardMarker = GuardNoSend"]
  feat -- "是" --> dd{"同时开了 deadlock_detection?"}
  dd -- "是" --> ce["compile_error!"]
  dd -- "否" --> send["crate::GuardMarker = GuardSend"]
  nosend --> rm["RawMutex::GuardMarker / RawRwLock::GuardMarker"]
  send --> rm
  rm --> pd["MutexGuard 的 PhantomData<(&mut T, GuardMarker)>"]
  pd --> auto["编译器推导守卫是否 Send"]

两个标记类型

lock_api/src/lib.rs 第 99–106 行:

/// Marker type which indicates that the Guard type for a lock is `Send`.
pub struct GuardSend(());

/// Marker type which indicates that the Guard type for a lock is not `Send`.
#[allow(dead_code)]
pub struct GuardNoSend(*mut ());

unsafe impl Sync for GuardNoSend {}

GuardSend 只包一个 (),自动实现 Send 和 Sync。GuardNoSend 包一个裸指针 *mut ():裸指针既不 Send 也不 Sync,所以 GuardNoSend 默认两样都没有,第 106 行再把 Sync 单独加回来。两个类型都不会被构造出来,它们只出现在 PhantomData 里。

MutexGuard 的字段是 marker: PhantomData<(&'a mut T, R::GuardMarker)>(mutex.rs 第 504–507 行)。PhantomData<X> 在自动 trait 上表现得和 X 一样,于是 GuardMarker = GuardNoSend 时整个守卫不是 Send。

把 custom-lock 实验里自旋锁的标记改成 GuardNoSend(--features nosend),再断言守卫是 Send,编译器把这条推导链完整打印出来(路径已缩短):

error[E0277]: `*mut ()` cannot be sent between threads safely
   --> src/lib.rs:160:19
    |
160 |     assert_send::<SpinlockGuard<'static, u64>>();
    |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^ `*mut ()` cannot be sent between threads safely
    |
    = help: within `lock_api::MutexGuard<'static, RawSpinlock, u64>`, the trait `Send` is not implemented for `*mut ()`
note: required because it appears within the type `GuardNoSend`
   --> lock_api-0.4.14/src/lib.rs:104:12
note: required because it appears within the type `(&'static mut u64, GuardNoSend)`
note: required because it appears within the type `PhantomData<(&'static mut u64, GuardNoSend)>`
note: required because it appears within the type `lock_api::MutexGuard<'static, RawSpinlock, u64>`
   --> lock_api-0.4.14/src/mutex.rs:504:12

报错里出现的是 *mut (),而不是「守卫不是 Send」:编译器报告的是推导链最底层那个不满足条件的类型。在 tokio 里看到「*mut () cannot be sent」时,往上找 GuardNoSend 和守卫类型就能定位到持锁的那一行。

MappedMutexGuard、ArcMutexGuard 等不靠 PhantomData 推导,而是手写 unsafe impl Send,条件里带 R::GuardMarker: Send(例如 mutex.rs 第 887–890 行)。ReentrantMutexGuard 的标记写死为 GuardNoSend(remutex.rs 第 618–621 行),不看 raw 锁选了什么。上游没有注释这一处;可重入锁按线程 id 记录 owner(ReentrantMutex),本卷推测这是守卫不能换线程的原因。

parking_lot 的选择:一个类型别名

src/lib.rs 第 32–39 行:

// If deadlock detection is enabled, we cannot allow lock guards to be sent to
// other threads.
#[cfg(all(feature = "send_guard", feature = "deadlock_detection"))]
compile_error!("the `send_guard` and `deadlock_detection` features cannot be used together");
#[cfg(feature = "send_guard")]
type GuardMarker = lock_api::GuardSend;
#[cfg(not(feature = "send_guard"))]
type GuardMarker = lock_api::GuardNoSend;

RawMutex(raw_mutex.rs 第 63 行)和 RawRwLock(raw_rwlock.rs 第 64 行)都写 type GuardMarker = crate::GuardMarker;,RawFairMutex 转发内部 RawMutex 的(raw_fair_mutex.rs 第 17 行)。所以 send_guard 一开,parking_lot 的 Mutex、FairMutex、RwLock 的全部守卫一起变成 Send;ReentrantMutex 的守卫不变。send_guard 在 Cargo.toml 里是 send_guard = [],不转发给任何依赖。

历史:

时间提交 / 版本内容
2018-03dd970ed「Allow customizing whether lock guards are Send」,首次发布于 0.6.0引入 GuardMarker 关联类型和 GuardNoSend;parking_lot 的锁写死 GuardNoSend
2020-05358b35f「Allow lock guards to be sent to other threads」,0.11.0(#240)提交说明第二行「But only if deadlock detection is not enabled.」;加 send_guard feature、上面的 compile_error! 和 README 里的一段说明

为什么默认是 !Send:上游的提交说明、README、代码注释只给了一条理由,就是上面注释里的「If deadlock detection is enabled, we cannot allow lock guards to be sent to other threads」。死锁检测把「本线程持有哪些锁」记在加锁线程自己的 ThreadData 里,加锁后 acquire_resource、解锁前 release_resource,两者都只操作当前线程的那份记录(死锁检测)。守卫换线程后由另一个线程解锁,和这种按线程记账的方式不相容。至于为什么在没开死锁检测时也默认 !Send,上游没有写;本卷的推测是:两个 feature 互斥,默认值必须与 deadlock_detection 兼容,否则任何依赖开了死锁检测都会编译失败。

parking_lot 的 RawMutex 自身并不要求同一线程解锁:它的状态只是一个 AtomicU8,unlock 不读线程 id。下面 E9 ② 的多线程运行时那组里,有一次守卫在一个 worker 线程上加锁、任务醒来后在另一个 worker 线程上解锁,程序正常结束。

Cargo feature 是全图统一的

Cargo 把同一个包在依赖图里被打开的 feature 取并集(feature unification)。对 parking_lot 来说这意味着:

  • 依赖树里任何一个 crate 写了 parking_lot = { features = ["send_guard"] },整个程序里所有 parking_lot 守卫都是 Send,包括你自己代码里的。
  • 另一个 crate 打开了 deadlock_detection(例如为了调试),两个 feature 在 parking_lot 里相遇,报错出现在编译 parking_lot 时,而不是在开 feature 的那个 crate 里。

E9 ① 第三种组合的输出(exp-parking_lot/guard-await/,--features sg,dd,registry 路径已缩短):

    Checking parking_lot v0.12.5
error: the `send_guard` and `deadlock_detection` features cannot be used together
  --> parking_lot-0.12.5/src/lib.rs:35:1
   |
35 | compile_error!("the `send_guard` and `deadlock_detection` features cannot be used together");
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: could not compile `parking_lot` (lib) due to 1 previous error

找出是谁打开了哪个 feature,用 cargo tree -e features -i parking_lot。同一个实验 crate 开 sg 时的输出:

parking_lot v0.12.5
├── parking_lot feature "arc_lock"
│   └── guard-await v0.1.0 (exp-parking_lot/guard-await)
│       ├── guard-await feature "default" (command-line)
│       └── guard-await feature "sg" (command-line)
├── parking_lot feature "default"
│   └── guard-await v0.1.0 (exp-parking_lot/guard-await) (*)
└── parking_lot feature "send_guard"
    └── guard-await feature "sg" (command-line)

最后一个分支说明 send_guard 是谁打开的。本站依赖图里 parking_lot 开了哪些 feature 见 在本站中。

在 tokio 里持锁跨 .await(E9)

exp-parking_lot/guard-await/,parking_lot 0.12.5 + tokio 1.54.1,--offline --release。

① 编译:三种 feature 组合

程序就是在 tokio::spawn 的 async 块里持有守卫跨过一次 sleep(...).await:

let h = tokio::spawn(async move {
  let mut g = m2.lock();
  tokio::time::sleep(Duration::from_millis(10)).await;
  *g += 1;
});
组合结果
默认编译失败,错误全文见下
send_guard编译通过
send_guard + deadlock_detectionparking_lot 自身编译失败,compile_error!(上一节)

默认组合的完整错误(cargo check,路径已缩短):

error: future cannot be sent between threads safely
   --> src/bin/compile.rs:11:11
    |
 11 |     let h = tokio::spawn(async move {
    |  ___________^
 12 | |     let mut g = m2.lock();
 13 | |     tokio::time::sleep(Duration::from_millis(10)).await;
 14 | |     *g += 1;
 15 | |   });
    | |____^ future created by async block is not `Send`
    |
    = help: within `{async block@src/bin/compile.rs:11:24: 11:34}`, the trait `Send` is not implemented for `*mut ()`
note: future is not `Send` as this value is used across an await
   --> src/bin/compile.rs:13:51
    |
 12 |     let mut g = m2.lock();
    |         ----- has type `parking_lot::lock_api::MutexGuard<'_, parking_lot::RawMutex, u32>` which is not `Send`
 13 |     tokio::time::sleep(Duration::from_millis(10)).await;
    |                                                   ^^^^^ await occurs here, with `mut g` maybe used later
note: required by a bound in `tokio::spawn`
   --> tokio-1.54.1/src/task/spawn.rs:176:21
    |
174 |     pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
    |            ----- required by a bound in this function
175 |     where
176 |         F: Future + Send + 'static,
    |                     ^^^^ required by this bound in `spawn`

error: could not compile `guard-await` (bin "compile") due to 1 previous error

help 行说的是 *mut (),note 行才给出守卫类型 parking_lot::lock_api::MutexGuard<'_, parking_lot::RawMutex, u32>。std 守卫的同类错误和几种改法在 tokio 卷 future cannot be sent between threads safely。

② 运行:开了 send_guard 之后

编译通过不代表能用。src/bin/run.rs:任务 A 加锁后 sleep(10ms).await,守卫一直活着;等 A 确实拿到锁后再 spawn 任务 B,B 调用同一把锁的 lock()。另起一个普通线程当看门狗,2 秒后打印 HANG 并以退出码 2 结束进程。每种模式跑 3 次:

模式运行时锁结果(3 次)B 等锁的时间
pl-currentcurrent_threadparking_lot3/3 卡死,看门狗在 2.00–2.01 s 触发—
tokio-current(反向)current_threadtokio::sync::Mutex3/3 正常结束15.7–16.5 ms
pl-multi(反向)多线程,2 个 workerparking_lot3/3 正常结束12.6–16.5 ms

pl-current 的一次输出:

   0.019 ms  [ThreadId(1)] mode = pl-current
   0.062 ms  [ThreadId(1)] A: locked, sleeping 10 ms with the guard alive
   0.072 ms  [ThreadId(1)] B: calling lock()
2000.281 ms  [ThreadId(2)] HANG: watchdog fired after 2 s

卡死的过程:A 在 .await 处让出,唯一的线程转去执行 B;B 的 lock() 发现锁被占,自旋后在停车场里睡下(一次有竞争的加锁)。这一睡把运行时唯一的线程带走了,时间驱动没人推进,A 的 sleep 永远不会被唤醒,A 也就永远不会释放锁。parking_lot 的锁不知道 tokio 的存在,它阻塞的是 OS 线程。

两组反向验证:

  • 把锁换成 tokio::sync::Mutex,B 的 lock().await 拿不到锁时让出线程,A 的定时器照常触发,B 在 A 释放后拿到锁。等待时间比 10 ms 多出的几毫秒来自 tokio 定时器的粒度。
  • 换成 2 个 worker 的多线程运行时,B 阻塞了它所在的那个 worker,另一个 worker 推进定时器、恢复 A,A 解锁后 B 继续。这不是「没问题」:B 卡住的十几毫秒里,那个 worker 上排队的其他任务都在等。第 3 次运行里,A 在 ThreadId(3) 上加锁、在 ThreadId(4) 上醒来并解锁,B 则阻塞在 ThreadId(3) 上,这就是 send_guard 允许的「守卫换线程」。

所以 send_guard 只是让编译器不再拦你,持锁跨 .await 的阻塞问题照旧。临界区里有 .await 时该用 tokio::sync::Mutex,或者把临界区缩到 .await 之前;tokio 卷的 tokio 的 Mutex 还是 std 的 Mutex 有两种锁的开销实测和选择表。本卷的常见坑汇总在 09 常见坑。

小结

  • GuardNoSend(*mut ()) 靠裸指针让 PhantomData 推出 !Send,再单独 unsafe impl Sync;GuardSend(()) 什么都不挡。守卫是否 Send 完全由 raw 锁选的标记决定。
  • 编译错误里出现的是推导链最底层的 *mut (),守卫类型在 note 行。
  • parking_lot 的 send_guard 只改 crate::GuardMarker 这一个类型别名,影响 Mutex / FairMutex / RwLock 的所有守卫;ReentrantMutexGuard 恒为 !Send。
  • 上游写明的理由只有死锁检测:它按加锁线程记账,所以两个 feature 同开就 compile_error!;默认关的原因上游没写,本卷推测是为了与 deadlock_detection 兼容。
  • feature 是全图统一的:任何依赖开了 send_guard 都会改变你的守卫类型,cargo tree -e features -i parking_lot 找出是谁。
  • E9:开 send_guard 后持锁跨 .await 能编译,但 current_thread 运行时 3/3 卡死;tokio::sync::Mutex 和多线程运行时作为反向验证都正常结束,后者在 B 等锁期间占住一个 worker 十几毫秒。

💬 关联讨论

发起讨论
加载中...