Прерывание клавиатуры от Python не прерывает работу Rust (PyO3)

У меня есть библиотека Python, написанная на Rust с PyO3, и она требует некоторых дорогостоящих вычислений (до 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.


person Neven V.    schedule 13.06.2020    source источник
comment
Прерывание потока в скомпилированном языке, таком как Rust, не является безопасной операцией: это оставит процесс в неопределенном состоянии.   -  person mcarton    schedule 13.06.2020
comment
Мои темы о ржавчине доступны только для чтения. Есть ли способ обойти это ограничение?   -  person Neven V.    schedule 13.06.2020
comment
Что вообще означает поток только для чтения? Темы нельзя безопасно отменить, и точка. Теперь фьючерсы async предназначены для отмены в определенных точках (например, await), но я не знаю, поддерживает ли их PyO3.   -  person mcarton    schedule 13.06.2020


Ответы (2)


Одним из вариантов было бы создать отдельный процесс для запуска функции 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
person Brent Kerby    schedule 15.06.2020
comment
Могу подтвердить, что это работает, тот факт, что для этого требуется дополнительный код Python, немного мешает: я пишу библиотеку для других людей, а не внутренний скрипт. Однако я считаю, что это требование выходит за рамки этого вопроса. - person Neven V.; 15.06.2020
comment
Можно ли рассматривать модуль Python, сгенерированный PyO3, как внутренний, поместив его в другой модуль Python, который будет доступен библиотеке и будет содержать код, создающий подпроцесс? Тогда конечному пользователю не придется с этим сталкиваться. - person Brent Kerby; 15.06.2020
comment
Это может быть решением. Я рассмотрю возможность объединения библиотеки .so со скриптом Python, который импортирует ее в один модуль. Это избавит конечных пользователей от необходимости иметь дело с несколькими файлами. Моя библиотека PyO3 уже представляет собой оболочку для другого ящика, поэтому она создаст довольно много слоев, хотя это не так уж важно. - person Neven V.; 15.06.2020
comment
Я просто случайно обнаружил, что дополнительный код Python даже не нужен. Я добавил 1 зависимость к моей Cargo.toml, одну строку для use ctrlc; и один вызов ctrlc::set_handler в моей библиотеке Rust прямо перед вызовом дорогой функции. Задача решена. Весь шаблонный код Python не требуется. - person Neven V.; 16.06.2020
comment
Единственное, на что следует обратить внимание, это то, что если вы работаете из REPL, то нажатие Ctrl+C прервет не только функцию, но и весь сеанс. Это нормально для моего собственного варианта использования, но любой, кто найдет этот ответ в будущем, должен быть предупрежден. - person Neven V.; 16.06.2020
comment
Да, если вы удалите дополнительный код Python, функция Rust не будет работать в подпроцессе, поэтому ctrlc вызовет выход из основного процесса. Целью дополнительного кода Python было сделать так, чтобы восстановление было возможным (т. е., но поместив try-except вокруг вызова .join() для перехвата KeyboardInterrupt), гарантируя, что ctrlc завершает только дочерний процесс. Но если это не требуется, то, конечно, без этого намного проще. - person Brent Kerby; 16.06.2020

Ваша проблема очень похожа на этот, за исключением того, что ваш код написан на Rust вместо C++.

Вы не сказали, какую платформу вы используете - я предполагаю, что она похожа на unix. Некоторые аспекты этого ответа могут быть неверны для Windows.

В unix-подобных системах сочетание клавиш Ctrl+C приводит к отправке вашему процессу сигнала SIGINT. На самом низком уровне библиотеки C приложения могут регистрировать функции, которые будут вызываться при получении этих сигналов. Подробнее см. man signal(7). описание сигналов.

Поскольку обработчики сигналов могут быть вызваны в любое время (даже на полпути к какой-либо операции, которую вы обычно считаете атомарной), существуют большие ограничения на то, что на самом деле может делать обработчик сигналов. Это не зависит от языка программирования или среды. Большинство программ просто устанавливают флаг, когда они получают сигнал, а затем возвращаются, а затем проверяют этот флаг и предпринимают какие-либо действия.

Python ничем не отличается — он устанавливает обработчик сигнала для сигнала SIGINT, который устанавливает некоторый флаг, который он проверяет (когда это безопасно) и выполняет действие.

Это работает нормально при выполнении кода Python — он будет проверять флаг по крайней мере один раз для каждого оператора кода — но это другое дело при выполнении долго работающей функции, написанной на Rust (или любом другом иностранном языке). Флаг не проверяется, пока ваша функция ржавчины не вернется.

Вы можете улучшить ситуацию, проверив флаг в вашей функции ржавчины. PyO3 открывает PyErr_CheckSignals, которая делает именно это. Эта функция:

проверяет, был ли отправлен сигнал процессам, и если да, то вызывает соответствующий обработчик сигнала. Если модуль сигнала поддерживается, это может вызвать обработчик сигнала, написанный на Python. Во всех случаях по умолчанию для SIGINT возникает исключение KeyboardInterrupt. Если возникает исключение, устанавливается индикатор ошибки и функция возвращает -1; иначе функция возвращает 0

Таким образом, вы можете вызывать эту функцию с подходящими интервалами внутри вашей функции Rust и проверять возвращаемое значение. Если это было -1, вы должны немедленно вернуться из своей функции Rust; иначе продолжайте.

Картина становится более сложной, если ваш код на Rust многопоточный. Вы можете вызывать PyErr_CheckSignals только из того же потока, что и интерпретатор Python, вызывающий вас; и если он вернет -1, вам придется очистить все другие потоки, которые вы начали, прежде чем вернуться. Как именно это сделать, выходит за рамки этого ответа.

person harmic    schedule 15.06.2020
comment
Просто мой код сильно многопоточный, но я разберусь с этим. - person Neven V.; 15.06.2020
comment
Извините, что не принял ваш ответ, другой ответ дал мне более простое решение. - person Neven V.; 16.06.2020