如何在 Rust 子线程中捕获堆栈溢出?

3

我有一个递归算法,可能会非常深度嵌套(它是反编译器的一部分)。我知道通常情况下你不会只是增加堆栈大小,因为堆栈溢出通常表示无限递归,但在这种情况下,该算法有时可能只需要更大的堆栈,因此我在子线程中运行该算法,并使用CLI标志可以增加堆栈大小:

fn main() -> Result<(), Box<std::io::Error>> {
    // Process arguments
    let args: Cli = Cli::from_args();

    let child = thread::Builder::new()
        .name("run".into())
        .stack_size(args.stack_size * 1024 * 1024)
        .spawn(move || -> Result<(), Box<std::io::Error>> { run(args)?; Ok(()) })
        .unwrap();

    child.join().unwrap()?;

    Ok(())
}

fn run(args: Cli) -> Result<(), Box<std::io::Error>> {
    ...
}

这很好用,将--stack-size=20传递给应用程序,运行线程将获得20MB的栈,只要足够,它就可以快乐地运行。但是,第一次运行时只有默认8MB的堆栈,会出现以下错误:
thread 'run' has overflowed its stack
fatal runtime error: stack overflow

我希望能够捕获这个错误,并打印一条消息提示用户可以传递--stack-size=X以给反编译器提供更大的堆栈。
如何捕获Rust子线程的堆栈溢出?

我在这里找到了关于堆栈溢出恐慌的一些信息: "堆栈溢出是一种中止恐慌,它不会展开并且无法捕获。你需要编译器插入堆栈探测(有时称为堆栈敲击),并在探测失败时启动展开;这必然会带来性能损失。" - undefined
阅读更多的帖子,"stack probes" 可能会得到支持 链接 - undefined
@Todd 如果你需要捕获堆栈溢出,那么使用子进程而不是子线程可能会更好。然而,即使我将算法作为一个独立的进程运行,仍然会打印出“线程'run'已经溢出其堆栈”的消息... - undefined
至少它不会使你的应用程序崩溃,而且你的父进程可以确定子进程是否退出,并代表其打印出错误信息。另外,也许标准错误输出可以通过父进程发送,由父进程决定是否打印出来。 - undefined
1
最近我尝试将递归代码转换为迭代代码,并想起了这个问题。如果你仍然在寻找一种将递归代码转换的方法,也许这个链接可以帮到你。 - undefined
显示剩余3条评论
1个回答

2
“中止恐慌”:
堆栈溢出属于我所见过的被称为“中止恐慌”的类别。无法使用catch_unwind()捕获和恢复它们。 OP建议使用子进程将故障与应用程序的其余部分隔离开来,这似乎是一个合理的解决方法。
这是Reddit上有关“堆栈探测”(以及其他事情)的长线程。这可能是一种防止线程溢出的方法。如果您想了解更多信息,请查看Module compiler_builtins :: probestack的文档。
此参考资料的几个摘录:

堆栈探测的目的是提供静态保证,即如果线程具有警卫页,则保证堆栈溢出会命中该警卫页。

最后值得注意的是,在撰写本文时,LLVM仅支持x86和x86_64上的堆栈探测。
需要注意的是,堆栈探测功能并不完全安全。这对于大多数应用程序可能没有影响,但对于通过网站自动化提供的编译器等工具可能会有影响。
避免递归
递归算法可能更容易编写,但在许多情况下比循环迭代数据结构(如树)效率低。循环方法更难编写,但可以更快地运行并使用更少的内存。如果要解决的问题是树遍历,则有很多在线示例可供参考。一些避免递归的算法使用自己声明的堆栈,例如向量、列表或其他类似堆栈的结构。其他算法,如Morris遍历,则不需要维护堆栈数据结构。重新设计有问题的递归逻辑是减少堆栈溢出风险的一种方法。
实现自己的堆栈

关于如何将递归函数转换为迭代函数的 Python 通用方法,这里提供了一个示例。我在将递归多键快速排序代码转换为 Rust 后遇到了堆栈问题,因此编写了这个答案。我使用它来对后缀数组进行排序,需要数万次深度递归调用。按照所描述的方法进行转换后,应用程序能够处理非常大的文本块而无任何问题。

对于非致命的可恢复性恐慌:

如果恐慌不是致命的/不可恢复的,则可以使用 std::panic crate 捕获它们并获取诊断信息。

有关控制 panic 的更多信息,请参阅 Rust Edition Guide 中的 Controlling panics with std::panic 部分。这是关于 std::panic 文档的 链接

use std::env;
use std::thread;
use std::panic::catch_unwind;
use std::panic;

fn main() -> Result<(), Box<std::io::Error>> {
    let args = env::args().collect::<Vec<String>>();

    panic::set_hook(Box::new(move |panic_info| {
        match panic_info.location() {
            Some(loc) => {
                println!("Panic in file {} line {}.", loc.file(), loc.line());
            },
            None => { println!("No panic info provided..."); },
        }
    }));

    let child = thread::spawn(move || {
        catch_unwind(|| {
            run(args);
        });
    });

    child.join();
    
    Ok(())
}

fn run(args: Vec<String>) -> Result<(), Box<std::io::Error>> {
    println!("Args from command line: {:?}", args);
    panic!("uh oh!");
    Ok(())
}

嗯,谢谢你的建议,但我刚刚尝试过了,它并没有捕获堆栈溢出。堆栈溢出是一种恐慌吗?在我看来,它们似乎表现得不同。 - undefined
哦,不好意思,我不确定为什么堆栈溢出在这里无法工作。希望其他人能提供一些见解。@curiousdannii - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接