什么是panic
在Rust 中,有一类错误叫作panic。示例如下:
js
fn main(){
let x:Option<i32>=None;
x.unwrap();
}
编译,没有错误,执行这段程序,输出为:
js
thread '<main>'panicked at 'called `Option::unwrap() on a None`value',../
src/libcore/option.rs:326
note:Run with RUST_BACKTRACE=1`for a backtrace.
这种情况就引发了一个panic 。在这段代码中,我们调用了Option::unwrap()方法, 正是这个方法有可能导致panic。根据提示,我们设置一个环境变量RUST_BACKTRACE=1之后再执行这个程序,可以看到这个程序在发生panic 时候的函数调用栈。
执行RUST_BACKTRACE=1 ./test,结果为:
js
thread 'main'panicked at 'called `Option::unwrap()`on a None`value',../src/ libcore/option.rs:323
stack backtrace:
1:0x10af488f8-std::sys::backtrace::tracing::imp::write::h6f1d53a70916b90d
2:0x10af4a3af-std::panicking::default_hook::{{closure}}::h137e876f7d3b5850 3:0x10af49945 -std::panicking::default_hook::h0ac3811ec7cee78c
4:0x10af49e96-std::panicking::rust_panic_with_hook::hc303199e04562edf
5:0x10af49d34-std:: ::begin_panic::h6ed03353807cf54d
6:0x10af49c52-std::panicking::begin_panic_fmt::hc321cece241bb2f5
7:0x10af49bb7-rust_begin_unwind
8:0x10af6f0b0-core::panicking::panic_fmt::h27224b181f9f037f
9:0x10af6efb4-core::panicking::panic::h53676c30b3bd95eb
10:0x10af44804-<core::option::Option<T>>::unwrap::h3478e42c3c27faa3
11:0x10af44880-test::main::h8a7a35fa594c0174
12:0x10af4a96a-rust_maybe_catch_panic
13:0x10af49486-std::rt::lang_start::h538f8960e7644c80
14:0x10af448b9-main
我们去查一下Option::unwrap() 的文档,其中是这么说的:
js
Moves the value v out of the Option<T>if it is Some(V).
Panics
Panics if the self value equals None.
Safety note
In general,because this function may panic,its use is discouraged.
Instead,prefer to use pattern matching and handle the None case explicitly.
- 当 Option 内部的数据是Some 时,它可以成功地将内部的数据move 出来返回。
- 当 Option 内部的数据是None 时,它会发生panic 。panic 如果没有被处理,它会导致 整个程序崩溃。
在 Rust 中,正常的错误处理应该尽量使用Result类 型 。Panic 则是作为 一种 "fail fast" 机制,处理那种万不得已的情况。比如,上面例子中的unwrap 方法,试图把Option转换为i32 类型,当参数 是 None 的时候,这个转换是没办法做到的,这种时候就只能使用panic 。所以, 一般情况 下,用户应该使用unwrap_or等不会制造panic 的方法。
在Rust 中 ,Panic 的实现机制有两种方式:unwind 和 abort。
- unwind 方式在发生panic 的时候,会一层一层地退出函数调用栈,在此过程中,当前 栈内的局部变量还可以正常析构。
- abort方式在发生panic 的时候,会直接退出整个程序。
在常见的操作系统上,默认情况下,编译器使用的是unwind 方式。所以在发生panic 的时候,我们可以通过一层层地调用栈找到发生panic 的第一现场,就像前面例子展示的 那样。
但 是 ,unwind 并不是在所有平台上都能获得良好支持的。在某些嵌入式系统上, unwind 根本无法实现,或者占用的资源太多。在这种时候,我们可以选择使用abort 方式实现 panic。
编译器提供了一个选项,供用户指定panic 的实现方式。如下所示:
js
rustc -C panic=unwind test.rs
rustc -C panic=abort test.rs
可以试试上面两个编译命令,做一下对比。可以看到它们生成的代码,panic 时的行 为是不一样的,生成的可执行程序大小也不同。
Rust 中,通过unwind 方式实现的panic, 其内部的实现方式基本与C++ 的异常是一样 的。而且,Rust 提供了一些工具函数,可以让用户在代码中终止栈展开。示例如下:
js
use std::panic;
fn main(){
panic::catch_unwind(||{
let x:Option<i32>=None;
x.unwrap();
println!("interrupted.");
}).ok();
println!("continue to execute."); }
编译执行可见,在unwrap 语句后面的这条打印语句并未执行。因为在上一条语句中触 发了panic, 这个函数调用栈开始销毁。但是我们有一句catch_unwind阻止了函数调用栈 的继续展开,就像C++ 里面的try-catch 机制一样。因此,main 方法并没有继续被销毁,最 后那条语句可以正常打印输出。
如果我们尝试使用"-c panic=abort" 选项编译上面的代码,可以看到这个catch_unwind 起不了什么作用,最后那条语句无法正常打印输出。
但是,请大家注意,这个catch_unwind 机制绝对不是设计用于模拟"try-catch"机 制的。请大家永远不要利用这个机制来做正常的流程控制。 Rust 推荐的错误处理机制是用 返回值,第33章讲解Rust 的错误处理机制。 panic 出现的场景一般是:如果继续执行下去 就会有极其严重的内存安全问题,这种时候让程序继续执行导致的危害比崩溃更严重,此时 panic 就是最后的一种错误处理机制。它的主要用处参考下面的情况:
- 在FFI 场景下的时候,当C 语言调用了Rust 的函数,在Rust 内部出现了panic, 如果 这个panic 在 Rust 内部没有处理好,直接扔到C 代码中去,会导致C 语言产生"未定 义行为"(undefined behavior)。
- 某些高级抽象机制需要阻止栈展开,比如线程池。如果一个线程中出现了panic, 我 们希望只把这个线程关闭,而不至于将整个线程池"拖下水"。
C++ 中引入了"异常"这个机制之后,同时也带入了一个"异常安全"(exception safety) 的概念。对这个概念不熟悉的读者,建议阅读以下文档:
异常安全存在四种层次的保证:
- No-throw这种层次的安全性保证了所有的异常都在内部正确处理完毕,外部毫无 影 响 ;
- Strong exception safety------强异常安全保证可以保证异常发生的时候,所有的状态都 可以"回滚"到初始状态,不会导致状态不 一 致的问题;
- Basic exception safety------基 本 异 常 安 全 保 证 可 以 保 证 异 常 发 生 的 时 候 不 会 导 致 资 源泄 漏 ;
- No exception safety------没有任何异常安全保证。