本文节选自<<Rust语言圣经>>一书
欢迎大家加入Rust编程学院,一起学习交流:
QQ群:1009730433

panic深入剖析

在正式开始之前,先来思考一个问题: 假设我们想要从文件读取数据,如果失败,你有没有好的办法通知调用者为何失败?如果成功,你有没有好的办法把读取的结果返还给调用者?

panic!与不可恢复错误

上面的问题在真实场景,其实挺复杂的,让我们先做一个假设:文件读取操作发生在系统启动阶段。那么可以轻易得出一个结论,一旦文件读取失败,那么系统启动也将失败,这意味着该失败是不可恢复的错误,无论是因为文件不存在还是操作系统硬盘的问题,这些只是错误的原因不同,但是归根到底都是不可恢复的错误(梳理清楚当前场景的错误类型非常重要).

既然是不可恢复错误,那么一旦发生,只需让程序崩溃即可。对此,Rust为我们提供了panic!宏,当调用执行该宏时,程序会打印出一个错误信息,展开报错点往前的函数调用堆栈,最后退出程序.

切记,一定是不可恢复的错误,才调用panic!处理,你总不想系统仅仅因为用户随便传入一个非法参数就崩溃吧?所以,只有当你不知道该如何处理时,再去调用panic!.

调用panic!

首先,来调用一下panic!,这里使用了最简单的代码实现,实际上你在程序的任何地方都可以这样调用:

fn main() {panic!("crash and burn");
}

运行后输出:

thread 'main' panicked at 'crash!!1', src/main.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

以上信息包含了两条重要信息:

  • main函数所在的线程崩溃了,发生的代码位置是src/main.rs中的第3行第5个字符(去除该行前面的空字符)
  • 在使用时加上一个环境变量可以获取更详细的栈展开信息: RUST_BACKTRACE=1 cargo run

下面让我们针对第二点进行详细展开讲解。

backtrace栈展开

在真实场景中,错误往往涉及到很长的调用链甚至会深入第三方库,如果没有栈展开技术,错误将难以跟踪处理,下面我们来看一个真实的崩溃例子:

fn main() {let v = vec![1, 2, 3];v[99];
}

上面的代码很简单,数组只有3个元素,我们却尝试去访问它的第100号元素(数组索引从0开始),那自然会崩溃。

我们的读者里不乏正义之士,此时肯定要质疑,一个简单的数组越界访问,为何要直接让程序崩溃?是不是有些大题小做了?

如果有过C语言的经验,即使你越界了,问题不大,我依然尝试去访问,至于这个值是不是你想要的(100号内存地址也有可能有值,只不过是其它变量或者程序的!),抱歉,不归我管,我只负责取,你要负责管理好自己的索引访问范围。上面这种情况被称为缓冲区溢出,并可能会导致安全漏洞,例如攻击者可以通过索引来访问到数组后面不被允许的数据。

说实话,我宁愿程序崩溃,为什么?当你取到了一个不属于你的值,这在很多时候会导致程序上的逻辑bug! 有编程经验的人都知道这种逻辑上的bug是多么难发现和修复!因此程序直接崩溃,然后告诉我们问题发生的位置,最后我们对此进行修复,这才是最合理的软件开发流程,而不是把问题藏着掖着:

thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

好的,现在成功知道问题发生的位置,但是如果我们想知道该问题之前经过了哪些调用环节,该怎么办?那就按照提示使用RUST_BACKTRACE=1 cargo run来再一次运行程序:

thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:0: rust_begin_unwindat /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/std/src/panicking.rs:517:51: core::panicking::panic_fmtat /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/panicking.rs:101:142: core::panicking::panic_bounds_checkat /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/panicking.rs:77:53: <usize as core::slice::index::SliceIndex<[T]>>::indexat /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/slice/index.rs:184:104: core::slice::index::<impl core::ops::index::Index<I> for [T]>::indexat /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/slice/index.rs:15:95: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::indexat /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/alloc/src/vec/mod.rs:2465:96: world_hello::mainat ./src/main.rs:4:57: core::ops::function::FnOnce::call_onceat /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/ops/function.rs:227:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

上面的代码就是一次栈展开(也称栈回溯),它包含了函数调用的顺序,当然按照逆序排列:最近调用的函数排在列表的最上方. 因为咱们的main函数基本是最先调用的函数了,所以排在了倒数第二位,还有一个关注点,排在最顶部最后一个调用的函数是rust_begin_unwind,该函数的目的就是进行栈展开,呈现这些列表信息给我们。

要获取到栈回溯信息,你还需要开启debug标志,该标志在使用cargo run或者cargo build时自动开启(这两个方式是Debug运行方式). 同时,栈展开信息在不同操作系统或者Rust版本上也所有不同。

panic时的两种终止方式

当出现panic!时,程序提供了两种方式来处理终止流程: 栈展开直接终止.

其中,默认的方式就是栈展开,这意味着Rust会回溯栈上数据和函数调用,因此也意味着更多的善后工作,好处是给于充分的报错信息和栈调用信息,便于事后的问题复盘。直接终止,顾名思义,不清理数据就直接推出程序,善后工作交与操作系统来负责。

对于绝大多数用户,使用默认选择是最好的,但是当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,例如下面的配置修改Cargo.toml文件,实现在release模式下遇到panic直接终止:

[profile.release]
panic = 'abort'

线程panic后,程序会否终止?

长话短说,如果是main线程,则程序会终止,如果是其它子线程,该线程会终止。因此,尽量不要在main线程中做太多任务,将这些任务交由子线程去做,就算panic也不会导致整个程序的结束。

具体解析见panic原理剖析

何时该使用panic!

下面让我们大概罗列下合适适合使用panic,虽然原则上,你理解了之前的内容后,会自己作出合适的选择,但是罗列出来可以帮助你强化这一点。

先来一点背景知识,在前面章节我们粗略讲过Result<T,E>这个枚举类型,它是用来表示函数的返回结果:

enum Result<T, E> {Ok(T),Err(E),
}

当没有错误发生时,函数返回一个用Result类型包裹的值Ok(T),当错误时,返回一个Err(E)。对于Result返回我们有很多处理方法,最简单粗暴的就是unwrapexpect,这两个函数非常类似,我们以unwrap举例:

use std::net::IpAddr;
let home: IpAddr = "127.0.0.1".parse().unwrap();

上面的parse方法试图将字符串"127.0.0.1"解析为一个IP地址类型IpAddr,它返回一个Result<IpAddr,E>类型,如果解析成功,则把Ok(IpAddr)中的值赋给home,如果失败,则不处理Err(E),而是直接panic

因此unwrap简而言之:成功则返回值,失败则panic, 总之不进行任何错误处理。

示例、原型、测试

这些场景,需要快速的搭建代码,错误处理反而会拖慢实现速度,也不是特别有必要,因此通过unrapexpect等方法来处理是最快的。

同时,当我们准备做错误处理时,全局搜索这些方法,也可以不遗漏的进行替换。

你确切的知道你的程序是正确时,可以使用panic

因为panic的触发方式比错误处理要简单,因此可以让代码更清晰,可读性也更加好,当我们的代码注定是正确时,你可以用unrawp等方法直接进行处理,反正也不可能panic

use std::net::IpAddr;
let home: IpAddr = "127.0.0.1".parse().unwrap();

例如上面的例子,"127.0.0.1"就是ip地址,因此我们知道parse方法一定会成功,那么就可以直接用unwrap方法进行处理。

当然,如果该字符串是来自于用户输入,那在实际项目中,就必须用错误处理的方式,而不是unwrap,否则你的程序一天要崩溃几十万次吧!

可能导致全局有害状态时

有害状态大概分为几类:

  • 非预期的错误
  • 之后代码的运行会受到明显可见的影响
  • 内存安全的问题

当错误预期会出现时,返回一个错误较为合适,例如解析器接收到格式错误的数据,HTTP请求接收到错误的参数甚至该请求内的任何错误(不会导致整个程序有问题,只影响该此请求)。 因为错误是可预期的,因此也是可以处理的

当启动时某个流程发生了错误,导致了后续代码的允许造成影响,那么就应该使用panic,而不是处理错误后,继续运行,当然你可以通过重试的方式来继续。

上面提到过,数组访问越界,就要panic的原因,这个就是属于内存安全的范畴,一旦内存访问不安全,那么我们无法保证自己的程序会怎么运行下去,也无法保证逻辑和数据的正确性。

panic原理剖析

本来不想写这块儿内容,因为真的难写,但是转念一想,既然号称圣经,那么本书就得与众不同,避重就轻显然不是该有的态度。

当调用panic!宏时,它会

  1. 格式化panic信息,然后使用该信息作为参数,调用std::panic::panic_any()函数
  2. panic_any会检查应用是否使用了panic hook,如果使用了,该hook函数会被调用
  3. hook函数返回后,当前的线程就开始进行栈展开:从panic_any开始,如果寄存器或者栈因为某些原因信息错乱了,那很可能该展开会发生异常,最终线程会直接停止,展开也无法继续进行。
  4. 展开的过程是一帧一帧的去回溯整个栈,每个帧的数据都会随之被丢弃,但是在展开过程中,你可能会遇到被用户标记为catching的帧(通过std::panic::catch_unwind()函数标记),此时用户提供的catch函数会被调用,展开也随之停止: 当然,如果catch选择在内部调用std::panic::resume_unwind()函数,则展开还会继续。

还有一种情况,在展开过程中,如果展开本身panic了,那展开线程会终止,展开也随之停止。

一旦线程展开被终止或者完成,最终的输出结果是取决于哪个线程panic:对于main线程,操作系统提供的终止功能core::intrinsics::abort()会被调用,最终结束当前的panic进程;如果是其它子线程,那么线程就会简单的终止,同时信息会在稍后通过std::thread::join()进行收集.

Rust学习教程30 - Panic原理剖析相关推荐

  1. Rust学习教程02 - Rust语言简介

    本文节选自<<Rust语言圣经>>一书 欢迎大家加入Rust编程学院,一起学习交流: QQ群:1009730433 进入Rust编程世界 一.Rust发展历程 Rust 最早是 ...

  2. Go框架 gin 源码学习--路由的实现原理剖析

    往期回顾: gin源码解析 - gin 与 net/http 的关系 gin 源码解析 - 详解http请求在gin中的流转过程 上面两篇文章基本讲清楚了 Web Server 如何接收客户端请求,以 ...

  3. java架构学习——5. 线程池原理剖析锁的深度化

    本篇博文主要包含: 线程池的基本概念 ThreadPoolExecutor 线程池四种创建方式 -newCachedThreadPool:可缓存线程池 -newFixedThreadPool:定长线程 ...

  4. mysql 实战 45讲 学习笔记 基础知识 原理剖析

    MySQL 实战45讲 持续更新中~ 00讲 开篇 我们知道如何写出逻辑正确的SQL语句来实现业务目标,却不确定这个语句是不是最优的 我们听说了一些使用数据库的最佳实践,但是更想了解为什么这么做 我们 ...

  5. Rust学习教程10 - 所有权

    本文节选自<<Rust语言圣经>>一书 欢迎大家加入Rust编程学院,一起学习交流: QQ群:1009730433 所有权 所有的程序都必须和计算机内存打交道,如何从内存中申请 ...

  6. Rust学习教程32 - 动态数组Vec

    本文节选自<<Rust语言圣经>>一书 欢迎大家加入Rust编程学院,一起学习交流: QQ群:1009730433 动态数组Vector 动态数组类型用Vec<T> ...

  7. Rust学习教程03 - 安装Rust环境

    本文节选自<<Rust语言圣经>>一书 欢迎大家加入Rust编程学院,一起学习交流: QQ群:1009730433 安装Rust 强烈建议使用rustup来安装Rust,当然如 ...

  8. 强化学习教程(四):从PDG到DDPG的原理及tf代码实现详解

    强化学习教程(四):从PDG到DDPG的原理及tf代码实现详解 原创 lrhao 公众号:ChallengeHub 收录于话题 #强化学习教程 前言 在前面强化学习教程(三)中介绍了基于策略「PG」算 ...

  9. iPhone/Mac Objective-C内存管理教程和原理剖析

    版权声明 此文版权归作者Vince Yuan (vince.yuan#gmail.com)所有.欢迎非营利性转载,转载时必须包含原始链接http://vinceyuan.cnblogs.com/,且必 ...

  10. python原理书籍_python书籍推荐:《深入浅出深度学习:原理剖析与Python实践》

    在过去的这十年,深度学习已经席卷了整个科技界和工业界,2016年谷歌阿尔法狗打败围棋世界冠军李世石,更是使其成为备受瞩目的技术焦点. 今日,小编就为大家推荐一本能让初学者和"老司机" ...

最新文章

  1. android byte[] 转string 好多问号_java程序员面试遇到string题如何不凉?
  2. S1 商品信息管理系统
  3. Java的时间为何从1970年1月1日开始
  4. python3.7操作kafka_python3.7新增关键字:async、await;带来和kafka-python==1.4.2的兼容性问题...
  5. 颜值当道,画风为王——桌游美术风格漫谈
  6. 最短路算法模板--SPFA
  7. redis一主一从一哨兵,第一次主从切换成功,再次主从切换无法正常执行?
  8. NDIS与WinSock关系之自我扫盲
  9. 20160818_周报日志之二
  10. 第二轮冲刺-Runner站立会议06
  11. linux mrtg,鳥哥的 Linux 私房菜 -- MRTG 流量偵測法
  12. Android中使用FragmentManager获得控件
  13. 2 网段 Vlan+NAT 配置
  14. Veritas Backup Exec 21安装
  15. 解决NVIDIA官网打开速度过慢的 CUDA下载界面打开过慢
  16. pci和pcie的区别
  17. 计算机如何重新连接打印机,电脑重装系统后怎么连接打印机
  18. 队爷的讲学计划 (强连通缩点+最短路)
  19. QQ秀文字,很酷的!
  20. SuperMap 地图裁剪

热门文章

  1. Newton tangent method
  2. 用Python告诉你深圳房租有多高
  3. 高等数学笔记-苏德矿-第十章-曲线积分和曲面积分-第七节-高斯公式与斯托克斯公式
  4. 关于树莓派DSI屏幕触摸不准的问题
  5. 京东2022双11预售期,商家该做些什么?
  6. 微信支付和分享到朋友圈-struts版本
  7. 数据挖掘--数据流挖掘
  8. 微信小程相对图片路径_微信小程序加载本地图片时“../”的使用
  9. 开发QQ互联ios版Ane扩展 辛酸史
  10. mysql横切竖切_MySQL常用操作 - cheney-f的个人空间 - OSCHINA - 中文开源技术交流社区...