通过 std::panic 处理崩溃

Minimum Rust version: 1.9

有一个 std::panic 模块,其中包含崩溃,停止和启动的展开过程的方法:


#![allow(unused)]
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 配对,然后可以用它来重新启动它所属的池的客户端上的崩溃过程。

在这两种情况下,您都在一个线程中引入了一个新的隔离边界,然后将崩溃转换为其他地方的其他形式的错误。