如何捕获 panic 并继续运行?
解读
国内面试中,这道题常被用来区分“写过 Rust”与“写过生产级 Rust”。
面试官真正想听的是:
- 你不会用
std::panic::catch_unwind去替代正常的Result错误处理; - 你知道跨 FFI 边界或线程边界时 panic 的默认行为是 abort,必须显式处理;
- 你能讲清楚 UnwindSafe 与 RefUnwindSafe 这两个 auto trait 的约束,以及为什么
Mutex可以而Cell不行; - 你能给出最小可复用的封装,让主线程继续跑,而不是简单打印一行 “panic caught”。
知识点
std::panic::catch_unwind<F: FnOnce() -> R + UnwindSafe, R>(f: F) -> Result<R, Box<dyn Any + Send>>- UnwindSafe / RefUnwindSafe:标记在 panic 跨栈展开时是否会出现内存不安全;
- panic=abort vs panic=unwind 编译模式;Cargo 默认 dev 用 unwind,release 可手动切换;
- 线程默认 panic 策略:子线程 panic 会调用
std::panic::resume_unwind,主线程默认 abort; - FFI 场景:C 调用 Rust,Rust 侧一旦 panic 跨 FFI 边界即 UB,必须用
catch_unwind把错误转成错误码; - no_std 环境:
catch_unwind依赖 unwind 运行时,嵌入式若使用panic_abort则无法捕获; - Poisoning:
Mutex在 panic 后会被“毒化”,下一次加锁返回PoisonError,需要显式处理。
答案
use std::panic;
use std::sync::{Arc, Mutex};
use std::thread;
/// 把可能 panic 的闭包包起来,返回 `Result<T, String>`,主线程可继续跑
fn safe_run<F, R>(f: F) -> Result<R, String>
where
F: FnOnce() -> R + panic::UnwindSafe,
{
panic::catch_unwind(f).map_err(|e| {
match e.downcast::<String>() {
Ok(s) => *s,
Err(_) => "unknown panic payload".to_string(),
}
})
}
fn main() {
// 1. 同线程捕获
let r = safe_run(|| {
panic!("oops");
});
println!("caught: {:?}", r); // Err("oops")
// 2. 子线程捕获并继续
let flag = Arc::new(Mutex::new(0));
let flag_clone = Arc::clone(&flag);
let handle = thread::spawn(move || {
let _ = safe_run(|| {
panic!("child panic");
});
*flag_clone.lock().unwrap() = 1; // 线程未死,继续干活
});
handle.join().unwrap();
assert_eq!(*flag.lock().unwrap(), 1);
// 3. FFI 场景:暴露给 C 的函数
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
match panic::catch_unwind(|| a + b) {
Ok(sum) => sum,
Err(_) => -1, // 错误码
}
}
}
要点回顾:
- 闭包必须实现 UnwindSafe;如果内部有
Cell等内部可变容器,需用std::panic::AssertUnwindSafe包装,但必须自己保证安全性; - 不要滥用:
catch_unwind只能做最后的“救生网”,业务错误一律用Result; - 编译时加
-C panic=abort后,本技巧失效,需在 profile 里显式保留panic = "unwind"。
拓展思考
- Tokio 任务 panic:异步任务默认会调用
resume_unwind,导致整个运行时退出;生产环境需打开tokio::task::Builder::new().name(..).catch_unwind(true)才能隔离; - WebAssembly:
catch_unwind在wasm32-unknown-unknown下直接编译失败,因为无 unwind 表;若需捕获,只能改用panic_abort并在 JS 侧捕获RuntimeError; - 嵌入式 Rust:
cortex-m-rt使用panic_halt或panic_reset,无法展开;若需“容错继续”,必须把可能失败的逻辑放到独立线程或协程,并通过消息队列重启; - 性能敏感服务:高频调用路径下,
catch_unwind有额外一次__gcc_personality_v0展开开销;实测在 100w 次/秒场景下,延迟增加 5~8%,此时应把不稳定代码拆到独立进程,用 IPC 重启策略代替。