1. 综述:
在 Rust 中,默认情况下,线程的 panic 只会终止当前线程,不会导致整个进程崩溃(主线程除外);但存在特殊场景(通过 panic::catch_unwind 捕获或配置线程 panic 策略),行为会有差异。
两种场景:
- 默认行为(未捕获 panic):单个线程触发 panic 后,会打印错误回溯栈帧信息并终止当前线程,其他线程不受影响,进程会继续运行(不包括主线程)。
- 特殊场景:若通过 std::panic::catch_unwind 捕获了线程的 panic(需线程函数满足 UnwindSafe trait),则当前线程也不会终止,可继续执行后续逻辑。若主线程触发 panic 且未被捕获,则所有线程终止,整个进程终止。
在 Rust 中,panic 会终止当前线程,但不会回传至主线程或导致主线程中断。若主线程发生 panic,则会终止所有线程并使程序以代码 101 退出。即使子线程中出现 panic,该 panic 也无法回传至父线程或导致父线程中断
简单说:Rust 设计上避免了 “单个子线程 panic 拖垮整个进程”,默认仅终止出错线程,灵活性更高。
【我的理解总结】:
- 只要主线程活着,则进程就活着;否则进程死。
- std::panic::catch_unwind只在unwind模式下可以捕获满足std::panic::UnwindSafe trait的闭包中的panic, 或是被std::panic::AssertUnwindSafe包裹的闭包;而在abort模式下无效。
catch_unwind in std::panic – Rust
2.验证代码编译运行环境:
线上:rust playground 2024 stable;
线下: windows11家庭版 rust1.90 stable;
编译通过;运行成功。
Rust Playground
3.panic不同场景下的代码例子
(1)某个子线程发生panic不影响其他线程。
use std::thread;
use std::time::Duration;
fn main() {
println!("=== 示例1:线程panic仅终止自身 ===
");
// 子线程1:正常执行,不触发panic
let thread1 = thread::spawn(|| {
for i in 1..=5 {
println!("子线程1(正常):第{}次执行", i);
thread::sleep(Duration::from_secs(1)); // 休眠1秒,模拟任务耗时
}
println!("子线程1:任务执行完毕,正常退出");
});
// 子线程2:执行到第3次时触发panic
let thread2 = thread::spawn(|| {
for i in 1..=5 {
if i == 3 {
// 主动触发panic
panic!("子线程2:执行到第{}次时触发panic!", i);
}
println!("子线程2(正常):第{}次执行", i);
thread::sleep(Duration::from_secs(1));
}
// 以下代码不会执行(panic后线程终止)
println!("子线程2:任务执行完毕(不会打印)");
});
// 等待两个子线程执行(主线程阻塞)
thread1.join().unwrap(); // 等待子线程1正常结束
match thread2.join() {
Ok(_) => println!("子线程2:正常退出(不会打印)"),
Err(e) => println!("
主线程捕获:子线程2已panic,错误信息:{:?}", e),
}
println!("
主线程:所有子线程处理完毕,主线程正常退出");
}
【运行结果】
• 子线程 2 在第 3 次执行时触发 panic 并终止,后续代码不再执行。
• 子线程 1 不受影响,会完整执行 5 次循环后正常退出。
• 主线程捕获子线程 2 的 panic 信息,最终正常退出(进程未崩溃)。
(2)用catch_unwind捕获 Panic,避免当前线程崩溃
use std::panic;
use std::thread;
use std::time::Duration;
fn main() {
println!("=== 示例2:用catch_unwind捕获panic,线程不终止 ===
");
let worker_thread = thread::spawn(|| {
// 用catch_unwind包裹可能触发panic的代码块
let result = panic::catch_unwind(|| {
for i in 1..=5 {
if i == 3 {
panic!("任务执行到第{}次时触发panic!", i);
}
println!("线程内任务:第{}次执行", i);
thread::sleep(Duration::from_secs(1));
}
"任务正常执行完毕" // 无panic时的返回值
});
// 处理捕获结果:无论是否panic,线程都继续执行
match result {
Ok(msg) => println!("
线程内捕获:任务正常结束,返回:{}", msg),
Err(panic_err) => {
println!("
线程内捕获:任务触发panic,错误信息:{:?}", panic_err);
println!("线程内:捕获panic后,继续执行后续逻辑(如资源清理)");
// 此处可添加资源清理、日志上报等逻辑
thread::sleep(Duration::from_secs(2)); // 模拟后续处理
println!("线程内:后续逻辑执行完毕");
}
}
});
// 等待子线程执行完毕
worker_thread.join().unwrap();
println!("
主线程:子线程正常退出(未因panic终止),主线程退出");
}
【运行结果】
- 子线程在第 3 次执行时触发 panic,但被 catch_unwind 捕获,未导致线程终止。
- 线程在捕获 panic 后,继续执行后续的 “资源清理”“日志打印” 逻辑。
- 主线程最终正常等待子线程执行完毕,无崩溃或异常。
(3)主线程发生panic且未被捕获的场景
主线程未捕获 panic 终止后,所有线程终止,整个进程终止。
use std::thread;
use std::time::Duration;
fn main() {
println!("主线程:启动子线程(非守护线程)");
// 启动一个非守护子线程:模拟执行关键任务(需5秒)
thread::spawn(|| {
for i in 1..=5 {
println!("子线程:正在执行关键任务(第{}秒)", i);
thread::sleep(Duration::from_secs(1));
}
println!("子线程:关键任务执行完毕,正常退出");
});
// 主线程执行1秒后,主动触发未捕获的panic
thread::sleep(Duration::from_secs(1));
panic!("主线程:触发未捕获panic,即将终止");
}
【运行结果观察】
主线程触发panic, 则所有线程终止,进而整个进程终止。
(4)主线程并未触发panic,只是比子线程先正常退出的场景
Rust 主线程发生 Panic 时,默认会触发整个进程立即终止,所有子线程(无论是否守护)都会被强制中断,不会等待子线程执行完毕。
主线程正常退出(无 Panic),且主线程不join子线程,则所有子线程终止,不会等待子线程执行完成,进而整个进程终止。
use std::thread;
use std::time::Duration;
use std::io::{self, Write};
fn main() {
let _handle = thread::spawn(|| {
for i in 1..=5 {
//println!("子线程:第 {} 秒运行中", i);
let msg = format!("子线程:第 {} 秒运行中",i);
std::io::stdout().write_all(msg.as_bytes()).unwrap();
thread::sleep(Duration::from_secs(1));
}
let done = "子线程:任务执行完毕";
//println!("子线程:任务执行完毕"); // 这句会正常打印
std::io::stdout().write_all(done.as_bytes()).unwrap();
std::io::stdout().flush().unwrap();
});
// 主线程正常延迟1秒后退出(无 Panic)
thread::sleep(Duration::from_secs(1));
println!("主线程:正常退出");
std::io::stdout().flush().unwrap();
//_handle.join().unwrap();
}
【运行结果】
主线程先退出,只看到子线程部分执行输出,而非执行完毕。
4.panic的两种运行模式
Rust panic分成两种运行模式:unwind vs abort ; 以上结论只是在unwind模式下成立,在abort模式下不成立, 总结如下:
- unwind 模式(栈展开):“优雅退出”——触发Panic 时会“栈展开”回溯释放当前线程栈上的资源(如调用变量析构函数),支持用 std::panic::catch_unwind 捕获 Panic,程序有机会恢复执行。
- abort 模式(强制终止):“暴力崩溃”—— 不清理资源(析构函数不被调用),整个进程 + 所有线程立即终止;换言之,触发Panic 时不做栈展开,直接调用操作系统接口终止进程,速度更快,但资源无法正常释放,且无法捕获 Panic。
(1)在项目的Cargo.toml中配置panic模式
[profile.dev]
opt-level = 0
debug = true
split-debuginfo = '...' # Platform-specific.
strip = "none"
debug-assertions = true
overflow-checks = true
lto = false
panic = 'unwind'
incremental = true
codegen-units = 256
rpath = false
[profile.release]
opt-level = 3
debug = false
split-debuginfo = '...' # Platform-specific.
strip = "none"
debug-assertions = false
overflow-checks = false
lto = false
panic = 'unwind'
incremental = false
codegen-units = 16
rpath = false
【rust编译器命令选项】
rustc -C panic=unwind test.rs
rustc -C panic=abort test.rs
【验证代码】
use std::panic;
fn main() {
// 尝试捕获 Panic
let result = panic::catch_unwind(|| {
panic!("触发 Panic!"); //abort模式下,仅输出:'触发Panic!'
});
match result {
Ok(_) => println!("未触发 Panic"),
Err(_) => println!("成功捕获 Panic(当前是 unwind 模式)"), //unwind模式下才会有此处输出!
}
}
【运行结果分析】
若在abort模式下,则触发panic时则立刻终止当前线程,catch_unwind不起作用,仅输出:触发Panic! 若在unwind模式下,则catch_unwind起作用捕获此panic, 则此线程后面的代码逻辑可以继续执行, 则会输出:触发Panic! 成功捕获 Panic(当前是 unwind 模式)
5.特别提示
(1)比较常用并且可能触发panic的方法:
1)Option::None and Result::Err => unwrap() expect() to panic
2)panic!(“panic提示信息”);
(2)这个 catch_unwind 机制绝对不是设计用于模拟 “try catch” 机制的。
6.异常安全的四个级别层次
- No-throw. 这种层次的安全性,保证了所有的异常都在内部正确处理完毕,外部毫无影响。
- Strong exception safety. 强异常安全保证,可以保证异常发生的时候,所有的状态都可以“回滚”到初始状 态,不会导致状态不一致问题。
- Basic exception safety. 基本异常安全保证,可以保证异常发生的时候,不会导致资源泄漏。
- No exception safety. 没有任何异常安全保证。
7.Rethrow Panic
use std::panic;
fn main() {
let result = panic::catch_unwind(|| {
panic!("oh no!, panic occured!");
});
println!("I am ok 1st", );
if let Err(err) = result {
println!("I am ok 2nd", );
panic::resume_unwind(err); //继续panic栈展开。
//println!("unreachable here", );
}
println!("unreachable here", );
}
主要用于FFI, 根据C代码中传出来的Err,在Rust代码中throw a panic. 我看到有资料说:panic::resume_unwind(err); 不会调用panic hook,你可以在上面代码不同的位置来set a panic hook, 来验证是否会触发调用panic hook.
8.Set a panic hook(设定一个panic的处理钩子)
use std::panic;
fn main() {
panic::set_hook(Box::new(|info| {
println!("Custom panic hook: {:?}", info);
}));
panic!("Normal panic");
}
Registers a custom panic hook, replacing any that was previously registered.
The panic hook is invoked when a thread panics, but before the panic runtime is invoked. As such, the hook will run with both the aborting and unwinding runtimes. The default hook prints a message to standard error and generates a backtrace if requested, but this behavior can be customized with the set_hook and take_hook functions.
The hook is provided with a PanicInfo struct which contains information about the origin of the panic, including the payload passed to panic! and the source code location from which the panic originated.
The panic hook is a global resource.
9.Take a panic hook
use std::panic;
fn main() {
panic::set_hook(Box::new(|_| {
println!("Custom panic hook");
}));
let _ = panic::take_hook();
panic!("Normal panic");
}
- 注销当前注册的panic钩子,并将其返回。
- 如果未注册自定义钩子,则将返回默认钩子。
10.将Panic信息写入Log
use std::panic;
use std::ops::Deref;
fn main() {
panic::set_hook(Box::new(|panic_info| {
let (filename, line) = panic_info.location()
.map(|loc| (loc.file(), loc.line()))
.unwrap_or(("<unknown>", 0));
let cause = panic_info.payload()
.downcast_ref::<String>()
.map(String::deref);
let cause = cause.unwrap_or_else(|| {
panic_info.payload()
.downcast_ref::<&str>().map(|s| *s)
.unwrap_or("<cause unknown>")
});
println!("Test A panic occurred at {}:{}: {}", filename, line, cause); //you can write panicinfo to log/file/io here.
}));
panic!("oh panic!");
}
11.What is unwind safety? that is panic safe.
(1)Panic引发的两个问题
A data structure is in a temporarily invalid state when the thread panics.
This broken invariant is then later observed.
简单讲:由于panic发生, 导致某个元素处于无效状态,而且这个无效元素可以被外部引用到,观察到!
反过来讲,只要以上2点同时成立,则必然是unwind not safe。当然就不是panic safe!
Types such as &mut T and &RefCell are examples which are not unwind safe. The general idea is that any mutable state which can be shared across catch_unwind is not unwind safe by default. This is because it is very easy to witness a broken invariant outside of catch_unwind as the data is simply accessed as usual.
Types like &Mutex, however, are unwind safe because they implement poisoning by default. They still allow witnessing a broken invariant, but they already provide their own “speed bumps” to do so.
共享不可变,可变不共享,按照这个Rust最高哲学原则之一来判定, 一般而言那些可变且共享的元素(包括内部可变性)就是不安全的, 故此不满足UnwindSafe 。
(2)询问Rust Compiler那些元素是UnWindSafe
use std::cell::RefCell;
use std::sync::Mutex;
//do ask rust compiler what types are unwindsafe.
fn implements<T: std::panic::UnwindSafe>() {}
fn main() {
//可变不共享,共享不可变!
//包括内部可变性!
//对于可变且共享的元素,可否证明安全?
//below all is UnwindSafe.
implements::<Option<i32>>();
implements::<&Option<i32>>();
implements::<&Mutex<i32>>();
//below all is not UnwindSafe.
implements::<&mut i32>();
implements::<&RefCell<i32>>();
}
注意:Mutex虽然是内部可变且共享元素, 但却是UnWindSafe的;当持有这个Mutex的线程panic时, 这个Mutex通过自身的Poisoned策略, 可以对外部所有线程证明,我中毒了,我被panic毒害了,所以你们可以自己选择是否信任使用我持有的数据!正式由于Mutex可以自证清白,所以Rust Compiler认为它是UnWindSafe的!由此推到出第3个原则:一个共享可变元素,经历panic后,如果可以对外证明宣称自己已中毒而不会再毒害他人, 则Rust Compiler认为这个元素就是UnWindSafe 。此为我的理解,若有谬误望请海涵指正。例子代码:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let lock = Arc::new(Mutex::new(0_u32));
let lock2 = lock.clone();
let _ = thread::spawn(move || -> () {
// This thread will acquire the mutex first, unwrapping the result of
// `lock` because the lock has not been poisoned.
let _guard = lock2.lock().unwrap();
// This panic while holding the lock (`_guard` is in scope) will poison
// the mutex.
panic!();
}).join();
// The lock is poisoned by this point, but the returned result can be
// pattern matched on to return the underlying guard on both branches.
let mut guard = match lock.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
println!("{}", *guard );
*guard += 1;
println!("{}", *guard );
assert_eq!(lock.is_poisoned(), true);
println!("poisoned: {}",lock.is_poisoned() );
}
(3)AssertUnwindSafe主动宣称我是UnWindSafe的,请Rust Compiler放行
use std::panic::{self, AssertUnwindSafe};
fn main() {
let mut variable = 4;
println!("{}",variable );
// This code will not compile because the closure captures `&mut variable`
// which is not considered unwind safe by default.
// panic::catch_unwind(|| {
// variable += 3;
// });
// This, however, will compile due to the `AssertUnwindSafe` wrapper
let result = panic::catch_unwind(AssertUnwindSafe(|| {
variable += 3;
}));
println!("{}",variable );
println!("{:?}",result );
}
(4)采用C++RAII模式
当Panic发生时, 那么在Unwind模式下,Rust保证自动调用每一个栈对象的析构函数(但forget主动放弃析构函数被调用的对象除外) , 从而保证内存和各种资源的有效释放清理。 但是如果是Abort模式 , 亦或直接调用了exit()或abort()等系统接口, 则进程当即死亡, 故而Rust 没有自动调用析构函数的机会,内存和资源只能泄露了, 由操作系统打扫战场。验证代码:
use std::ops::Drop;
struct ATest(i32);
impl Drop for ATest{
fn drop(&mut self){
println!("drop {}", self.0);
}
}
fn main(){
let _a = ATest(1);
//std::process::exit(-1); //不会执行析构函数。
//std::process::abort(); //不会执行析构函数。
//注意:需要在Cargo.toml下[profile.dev]配置panic='abort'
panic!("在abort模式下,触发panic,则不会执行析构函数,立即终止!");
}
提示:在修改Cargo.toml配置或代码后,发现没有看到预期结果时,第一确定文件已经保存,然后执行cargo clean后,再去cargo build/run.
【后记】:我是一个普通的c++老码农,Rust爱好者,如今已四十不惑,青丝白发生,人谈不上机智,然多年来敏而好学,不敢懈怠, 随处学随处记,笔耕不辍,坚信好脑瓜不如烂笔头!如今赋闲家中,捣腾出来这些粗鄙之文,分享出来,抛砖引玉!本人水平有限,许多时候也是不求甚解,匆促行文,故而文中难免存有谬误,望诸君海涵指正



















暂无评论内容