通过 std::panic
处理崩溃
有一个 std::panic
模块,其中包含崩溃,停止和启动的展开过程的方法:
#![allow(unused_variables)] fn main() { use std::panic; let result = panic::catch_unwind(|| { println!("hello!"); }); assert!(result.is_ok()); let result = panic::catch_unwind(|| { panic!("oh no!"); }); assert!(result.is_err()); }
通常,Rust区分操作失败的两种方式:
- 由于 预期的问题,就像找不到文件一样。
- 由于 意外问题,就像索引超出数组范围一样。
预期的问题通常来自您无法控制的情况; 应该为其环境可能抛出的任何内容准备健壮的代码。
在Rust中,预期的问题通过 Result
类型 来处理,它允许函数将有关问题的信息返回给调用者,然后调用者可以以细粒度的方式处理错误。
意外问题是错误:它们是由于合同或断言被违反而产生的。由于它们是意料之外的,因此以细粒度的方式处理它们是没有意义的。 相反,Rust通过崩溃采用“快速失败”方法,默认情况下解除发现错误的线程的堆栈(运行析构函数但没有其他代码)。 其他线程继续运行,但每当他们尝试与崩溃线程(无论是通过通道还是共享内存)进行通信时,都会发现崩溃。 因此,崩溃将执行中止到一些“隔离边界”,边界另一侧的代码仍然可以运行,并且可能以某种非常粗粒度的方式从崩溃中“恢复”。 例如,服务器不一定因为其中一个线程中的断言失败而需要关闭。
同样值得注意的是,程序可能会选择中止而不是放松,因此捕捉崩溃可能无效。如果你的代码依赖于 catch_unwind
,你应该将它添加到你的Cargo.toml:
[profile.debug]
panic = "unwind"
[profile.release]
panic = "unwind"
如果您的任何用户选择中止,他们将遇到编译时失败。
catch_unwind
API提供了一种在线程中引入新的隔离边界的方法。 有几个关键的刺激例子:
- 在其他语言中嵌入 Rust
- 管理线程的抽象
- 测试框架,因为测试可能会引起崩溃,你不希望它会杀死测试运行器
对于第一种情况,跨语言边界展开是未定义的行为,并且经常导致实践中的段错误。 允许捕获崩溃意味着您可以通过 C API 安全地公开 Rust 代码,并将展开转换为C侧的错误。
对于第二种情况,请考虑一个线程池库。如果池中的线程发生混乱,您通常不希望杀死线程本身,而是抓住崩溃并将其传递给池的客户端。
catch_unwind
API 与 resume_unwind
配对,然后可以用它来重新启动它所属的池的客户端上的崩溃过程。
在这两种情况下,您都在一个线程中引入了一个新的隔离边界,然后将崩溃转换为其他地方的其他形式的错误。