来自 Python 的键盘中断不会中止 Rust 函数 (PyO3)

Keyboard Interrupt from Python does not abort Rust function (PyO3)

我有一个用 PyO3 用 Rust 编写的 Python 库,它涉及一些昂贵的计算(单个函数调用最多 10 分钟)。从 Python 调用时如何中止执行?

Ctrl+C好像只是在执行结束后才处理,所以本质上没什么用

最小可重现示例:

# Cargo.toml

[package]
name = "wait"
version = "0.0.0"
authors = []
edition = "2018"

[lib]
name = "wait"
crate-type = ["cdylib"]

[dependencies.pyo3]
version = "0.10.1"
features = ["extension-module"]
// src/lib.rs

use pyo3::wrap_pyfunction;

#[pyfunction]
pub fn sleep() {
    std::thread::sleep(std::time::Duration::from_millis(10000));
}

#[pymodule]
fn wait(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_wrapped(wrap_pyfunction!(sleep))
}
$ rustup override set nightly
$ cargo build --release
$ cp target/release/libwait.so wait.so
$ python3
>>> import wait
>>> wait.sleep()

输入wait.sleep()后立即输入Ctrl + C,字符^C打印到屏幕上,但仅10秒后我终于得到

>>> wait.sleep()
^CTraceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyboardInterrupt
>>>

已检测到 KeyboardInterrupt,但在 Rust 函数调用结束之前一直未处理。有没有办法绕过它?

将 Python 代码放入文件并从 REPL 外部执行时,行为相同。

您的问题与 this one 非常相似,只是您的代码是用 Rust 而不是 C++ 编写的。

您没有说明您使用的是哪个平台 - 我假设它是类 unix 平台。对于 Windows.

,此答案的某些方面可能不正确

在类 unix 系统中,Ctrl+C 会导致 SIGINT 信号被发送到您的进程。在 C 库的最底层,应用程序可以注册函数,这些函数将在收到这些信号时调用。有关信号的更详细说明,请参阅 man signal(7)

因为信号处理程序可以在任何时候调用(甚至可以在某些您通常认为是原子的操作中调用一部分),所以信号处理程序实际可以做的事情有很大的限制。这与编程语言或环境无关。大多数程序只是在收到信号时设置一个标志,然后 return,然后检查该标志并对其采取行动。

Python 没有什么不同 - 它为 SIGINT 信号设置了一个信号处理程序,该信号处理程序设置了一些标志,它检查(当这样做是安全的时候)并采取行动。

这在执行 python 代码时工作正常 - 它会在每个代码语句中至少检查一次标志 - 但在执行用 Rust 编写的长 运行 函数(或任何其他外语)。在你的 rust 函数 returns.

之前,该标志不会被检查

您可以通过检查 rust 函数中的标志来改进问题。 PyO3 exposes the PyErr_CheckSignals 函数正是这样做的。这个函数:

checks whether a signal has been sent to the processes and if so, invokes the corresponding signal handler. If the signal module is supported, this can invoke a signal handler written in Python. In all cases, the default effect for SIGINT is to raise the KeyboardInterrupt exception. If an exception is raised the error indicator is set and the function returns -1; otherwise the function returns 0

因此,您可以在 Rust 函数内以适当的时间间隔调用此函数,并检查 returned 值。如果它是 -1,你应该立即从你的 Rust 函数中 return;否则继续。

如果您的 Rust 代码是多线程的,情况会更复杂。您只能从与调用您的 python 解释器相同的线程调用 PyErr_CheckSignals;如果它 returns -1 你将不得不清理你在 returning 之前启动的任何其他线程。具体如何做到这一点超出了这个答案的范围。

一个选项是为 运行 Rust 函数生成一个单独的进程。在子进程中,我们可以设置一个信号处理程序以在中断时退出进程。 Python 然后将能够根据需要引发 KeyboardInterrupt 异常。下面是如何操作的示例:

// src/lib.rs
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;
use ctrlc;

#[pyfunction]
pub fn sleep() {
    ctrlc::set_handler(|| std::process::exit(2)).unwrap();
    std::thread::sleep(std::time::Duration::from_millis(10000));
}

#[pymodule]
fn wait(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_wrapped(wrap_pyfunction!(sleep))
}
# wait.py
import wait
import multiprocessing as mp

def f():
    wait.sleep()

p = mp.Process(target=f)
p.start()
p.join()
print("Done")

这是我在按 CTRL-C 后在我的机器上得到的输出:

$ python3 wait.py
^CTraceback (most recent call last):
  File "wait.py", line 9, in <module>
    p.join()
  File "/home/kerby/miniconda3/lib/python3.7/multiprocessing/process.py", line 140, in join
    res = self._popen.wait(timeout)
  File "/home/kerby/miniconda3/lib/python3.7/multiprocessing/popen_fork.py", line 48, in wait
    return self.poll(os.WNOHANG if timeout == 0.0 else 0)
  File "/home/kerby/miniconda3/lib/python3.7/multiprocessing/popen_fork.py", line 28, in poll
    pid, sts = os.waitpid(self.pid, flag)
KeyboardInterrupt