早上看到了这篇文章 智能指针有可能会让你的应用崩溃, 下面分析一下
会导致 stack overflow 的代码
struct Node<T> { val: T, next: Option<Box<Node<T>>>,}struct LinkedList<T> { head: Option<Box<Node<T>>>,}impl<T> LinkedList<T> { fn new() -> Self { Self { head: None } } fn push_front(&mut self, val: T) { let next = self.head.take(); self.head = Some(Box::new(Node { val, next })); }} fn main() { let mut list = LinkedList::new(); for i in 0..1000000 { list.push_front(i); }}
playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=dfb32796d46df05fd6bcc4855fc11ae1
输出的结果:
thread 'main' has overflowed its stackfatal runtime error: stack overflowtimeout: the monitored command dumped core/playground/tools/entrypoint.sh: line 11: 8 Aborted timeout --signal=KILL ${timeout} "$@"
原文中给出了解释:
程序崩溃是因为LinkedList的智能指针头部的默认释放导致对下一个节点的递归调用,这不是尾递归的,无法优化。修复方法是手动覆盖LinkedList数据结构的析构函数方法,迭代地释放每个节点,而不需要递归。从某种意义上说,这违背了智能指针的目的——它们无法从程序员那里解放手动内存管理的负担。
但是这个解释还不够直观,也没有给出修复代码
接下来我们一起以更直白的方式看看这个 LinkedList 被 Drop 时到底发生了什么
我们先给 Node<T>
和 LinkedList<T>
加上 Drop trait
, 方便我们观察代码执行过程
struct Node<T> { val: T, next: Option<Box<Node<T>>>,}struct LinkedList<T> { head: Option<Box<Node<T>>>,}impl<T> LinkedList<T> { fn new() -> Self { Self { head: None } } fn push_front(&mut self, val: T) { let next = self.head.take(); self.head = Some(Box::new(Node { val, next })); }} impl<T> Drop for Node<T> { fn drop(&mut self) { println!("drop node begin"); let _ = self.next.take(); println!("drop node end"); }} impl<T> Drop for LinkedList<T> { fn drop(&mut self) { println!("drop linkedlist begin"); let _ = self.head.take(); println!("drop linkedlist end"); }} fn main() { let mut list = LinkedList::new(); for i in 0..1000000 { list.push_front(i); } println!("EOF");}
playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=61f7aad2bf8ddcd133a146cd88744e97
查看执行结果:
thread 'main' has overflowed its stackfatal runtime error: stack overflowtimeout: the monitored command dumped core/playground/tools/entrypoint.sh: line 11: 7 Aborted timeout --signal=KILL ${timeout} "$@"
跟原来的代码一致
查看标准输出:
EOFdrop linkedlist begindrop node begindrop node begindrop node begin(...)drop node begin
省略处全部都是 drop node begin
, 可见我们的程序在链式调用 Node<T>
的 drop
函数。因为 drop
一个 Node
就是依次 drop
它内部的 fields
(val
和 next
),当所有 fields
被 drop
完了,这个 Node
结构也就算被释放了
问题就在它内部这个 next
,它是一个链条,更准确的说应该是一个套娃?或是洋葱?!而默认的 Drop
机制是从内部(fields
)向外层依次释放,当我需要剥掉最外层时,却要等它里面那一层先剥完,里面的一层又要等更里面的一层…… 当层数过多时就导致了上面的 stack overflow
知道了原因,改起来就简单了,要剥哪一层就直接剥,不要等其它层,看代码:
// 可以不写,因为 LinkedList<T> 的 drop 已经把这里的 next 置为 None 了,这里只是为了演示函数调用过程impl<T> Drop for Node<T> { fn drop(&mut self) { println!("drop node begin"); let _ = self.next.take(); println!("drop node end"); }} impl<T> Drop for LinkedList<T> { fn drop(&mut self) { println!("drop linkedlist begin"); // let _ = self.head.take(); let mut node = self.head.take(); // Some(Box<Node>) while let Some(mut inner) = node { // inner is Box<Node{val, next: Some()}> node = inner.next.take(); // inner.next is None } // drop inner println!("drop linkedlist end"); }}
playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b8cce86d16ee776516e14fd031e75c6c
查看输出结果:
EOFdrop linkedlist begindrop node begindrop node enddrop node begindrop node end(...)drop linkedlist end
可见我们的套娃已经被一层层剥开了
问题
这个 LinkedList
的例子比较简单,对于它的 stack overflow
我们仔细一推敲就能找到问题所在。但是如果我们的程序运行了一年半载都没问题,忽然有一天就 stack overflow
了,并且还没啥线索,这个时候该咋整,想想都让人头大。
所以,能否在 stack overflow
发生时获取到函数调用栈(backtrace
) 呢?
带着这个问题一顿搜索, 找到两个讨论这个问题的链接:
How to diagnose a stack overflow
issue’s cause?
Great stack overflow error messages
后面这个 issue 中有人给出了一个 crate: backtrace-on-stack-overflow, 目前这个 crate 不支持 Windows
λ bat src/main.rsfn main() { unsafe { backtrace_on_stack_overflow::enable() }; f(92)} fn f(x: u64) { f(x)}λ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.01s Running `target/debug/so`Stack Overflow: 0: backtrace_on_stack_overflow::handle_sigsegv at /home/matklad/p/backtrace-on-stack-overflow/src/lib.rs:33:40 1: <unknown> 2: so::f at src/main.rs:6 3: so::f at src/main.rs:7:5 4: so::f at src/main.rs:7:5 5: so::f at src/main.rs:7:5 6: so::f at src/main.rs:7:5 7: so::f at src/main.rs:7:5 8: so::f at src/main.rs:7:5 9: so::f at src/main.rs:7:5 10: so::f at src/main.rs:7:5