thread::scope 与 std 的 scope
主线位置:不在三条主线上。它是 crossbeam 最早的功能(仓库第一天的提交就叫「Basic scoped threads」),也是第一个被标准库正式取代的:Rust 1.63 起有了
std::thread::scope,crossbeam 的文档把自己的版本标成 soft-deprecated。
flowchart TB
s["scope(f)"] --> cu["catch_unwind(f(&scope))"]
cu --> wg["drop 自己那份 WaitGroup,wg.wait():等所有嵌套的 Scope 克隆被丢弃"]
wg --> join["取出 handles 里还没被 join 的句柄,逐个 join,收集 Err"]
join --> m{"f panic 了?"}
m -->|是| ru["resume_unwind"]
m -->|否| r{"有子线程 panic?"}
r -->|否| ok["Ok(f 的返回值)"]
r -->|是| err["Err(Box<Vec<Box<dyn Any + Send>>>)"]
普通的 thread::spawn 要求闭包是 'static,因为编译器不知道线程什么时候结束,借用的栈变量可能先被释放。作用域线程的承诺是:作用域结束之前,里面 spawn 的线程一定都 join 掉了,所以可以借用作用域外的变量。
crossbeam 的实现
pub fn scope<'env, F, R>(f: F) -> thread::Result<R>
where
F: FnOnce(&Scope<'env>) -> R,
{
struct AbortOnPanic;
impl Drop for AbortOnPanic {
fn drop(&mut self) {
if thread::panicking() {
std::process::abort();
}
}
}
let wg = WaitGroup::new();
let scope = Scope::<'env> {
handles: SharedVec::default(),
wait_group: wg.clone(),
_marker: PhantomData,
};
// Execute the scoped function, but catch any panics.
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| f(&scope)));
// If an unwinding panic occurs before all threads are joined
// promote it to an aborting panic to prevent any threads from escaping the scope.
let guard = AbortOnPanic;
// Wait until all nested scopes are dropped.
drop(scope.wait_group);
wg.wait();
// Join all remaining spawned threads.
let panics: Vec<_> = scope
.handles
.lock()
.unwrap()
// Filter handles that haven't been joined, join them, and collect errors.
.drain(..)
.filter_map(|handle| handle.lock().unwrap().take())
.filter_map(|handle| handle.join().err())
.collect();
mem::forget(guard);
// If `f` has panicked, resume unwinding.
// If any of the child threads have panicked, return the panic errors.
// Otherwise, everything is OK and return the result of `f`.
match result {
Err(err) => panic::resume_unwind(err),
Ok(res) => {
if panics.is_empty() {
Ok(res)
} else {
Err(Box::new(panics))
}
}
}
}
几个部件:
handles: Arc<Mutex<Vec<Arc<Mutex<Option<JoinHandle<()>>>>>>>:每 spawn 一个线程往里推一个句柄。用户手动join时会take()掉,所以最后只 join 剩下的。WaitGroup:每个子线程拿到一个新的Scope值(handles是同一个Arc,wait_group是克隆,spawn 444–448 行),线程结束时这份克隆被 drop。wg.wait()等的是「所有Scope克隆都没了」,也就是所有能再 spawn 的地方都结束了,之后handles不会再增长,才能放心地把它取空。这就是嵌套 spawn 的支持方式:WaitGroup是它唯一的内部用户。- 生命周期擦除:子线程闭包是
'env的,std::thread::Builder::spawn要'static,spawn 里 先Box成dyn FnOnce() + Send + 'env,再transmute成'static。安全性全靠「作用域结束前一定 join」。 AbortOnPanic:等待和 join 期间如果发生 unwind(例如某个Mutex被 poison,unwrap()panic),子线程可能还在用借来的栈变量,只能abort。
子线程闭包的签名是 FnOnce(&Scope<'env>) -> T:多一个 &Scope 参数,用来在子线程里再 spawn(文档 81–112 行的例子解释了为什么不能直接借用外层的 s)。
门面 crate 把它重导出成 crossbeam::scope,只在 std feature 打开且不是 cfg(crossbeam_loom) 时有(src/lib.rs:82)。
std::thread::scope
std 在 1.63.0 稳定了自己的版本(#[stable(feature = "scoped_threads", since = "1.63.0")]),实现思路不同。library/std/src/thread/scoped.rs:141:
pub fn scope<'env, F, T>(f: F) -> T
where
F: for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T,
{
// ...
let result = catch_unwind(AssertUnwindSafe(|| f(&scope)));
// Wait until all the threads are finished.
while scope.data.num_running_threads.load(Ordering::Acquire) != 0 {
// SAFETY: this is the main thread, the handle belongs to us.
unsafe { scope.data.main_thread.park() };
}
// Throw any panic from `f`, or the return value of `f` if no thread panicked.
match result {
Err(e) => resume_unwind(e),
Ok(_) if scope.data.a_thread_panicked.load(Ordering::Relaxed) => {
panic!("a scoped thread panicked")
}
Ok(result) => result,
}
}
不收集句柄,只数「还在跑的线程」,最后一个结束的线程 unpark 主线程。子线程 panic 时,只要它的结果没被 join 取走,线程结果(Packet)析构时就把 a_thread_panicked 置真(lifecycle.rs:171)。
crossbeam::scope | std::thread::scope | |
|---|---|---|
| 返回值 | thread::Result<R>,要 .unwrap() | 直接是 T |
| 未 join 的子线程 panic | 返回 Err,内容是所有 panic 载荷的 Vec | scope 自己 panic:「a scoped thread panicked」,原载荷丢失 |
| 子线程闭包 | FnOnce(&Scope<'env>) -> T | FnOnce() -> T;嵌套 spawn 直接借用外层的 s:它是 &'scope Scope,活得和所有子线程一样久 |
| 等待方式 | WaitGroup + 逐个 join | 原子计数 + park / unpark |
| 每次 spawn 的额外开销 | 两个 Arc<Mutex<Option<…>>>(句柄、结果),推进共享 Vec 要拿锁 | 无额外锁 |
f 自己 panic | resume_unwind | resume_unwind |
文档里 soft-deprecated 的说明(thread.rs:138)是 2023-01 的 7df683c8 加的,理由写的是 std 版本「more efficient」。代码没有加 #[deprecated],编译不会警告。
实验 E12:子线程 panic 时两边怎么表现
断言:子线程 panic 且没被手动 join 时,crossbeam 返回 Err(Box<Vec<Box<dyn Any + Send>>>),std 直接 panic;没有 panic 时两边都正常返回;panic 的子线程被手动 join 过,两边在作用域层面都不再报告。
方法(exp-crossbeam/scope-panic):同一组情形分别用 crossbeam::scope 和 std::thread::scope 跑;std 那边包一层 catch_unwind 观察 panic 载荷;装一个空的 panic hook 让输出干净。
let res = crossbeam::scope(|s| {
let h = s.spawn(move |_| {
if child_panics { panic!("child boom"); }
7
});
if join_explicitly {
println!("explicit join -> is_err={}", h.join().is_err());
}
"scope body done"
});
结果(rustc 1.99.1,--release,跑 3 次输出相同):
crossbeam no-panic: Ok("scope body done")
std no-panic: returned "scope body done"
crossbeam child-panics: Err, is Vec<Box<dyn Any + Send>>=true, payloads=["child boom"]
std child-panics: panicked with "a scoped thread panicked"
[child-panics-joined] explicit join -> is_err=true
crossbeam child-panics-joined: Ok("scope body done")
[child-panics-joined] explicit join -> is_err=true
std child-panics-joined: returned "scope body done"
断言成立。要注意 std 那一行:原来的 panic 载荷 "child boom" 不在 scope 的 panic 里,只能在默认 panic hook 打印的那行日志里看到;crossbeam 把载荷原样交还。从 crossbeam 迁移到 std 时,原来写成 .unwrap() 的地方行为基本不变(都 panic),写成 match / if let Err 处理子线程 panic 的地方就要改成对每个句柄 join()。这条会在 常见坑 里再出现。
小结
- crossbeam 的
scope=catch_unwind(f)+WaitGroup等所有嵌套Scope结束 + 逐个 join 剩下的句柄;子线程 panic 收集成Err(Vec),f自己 panic 则继续传播。 - 子线程闭包多一个
&Scope参数,用来嵌套 spawn;闭包生命周期靠transmute擦成'static,等待期间再 panic 直接abort。 - std 1.63 的
std::thread::scope只数运行中的线程,未 join 的子线程 panic 会让scope自己 panic,载荷丢失;返回值不包Result。 - crossbeam 文档自 2023-01 起把它标为 soft-deprecated,但没有
#[deprecated]属性。