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 的选择:一个类型别名
// 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-03 | dd970ed「Allow customizing whether lock guards are Send」,首次发布于 0.6.0 | 引入 GuardMarker 关联类型和 GuardNoSend;parking_lot 的锁写死 GuardNoSend |
| 2020-05 | 358b35f「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_detection | parking_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-current | current_thread | parking_lot | 3/3 卡死,看门狗在 2.00–2.01 s 触发 | — |
tokio-current(反向) | current_thread | tokio::sync::Mutex | 3/3 正常结束 | 15.7–16.5 ms |
pl-multi(反向) | 多线程,2 个 worker | parking_lot | 3/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 十几毫秒。