Cython, Python и KeyboardInterrupt игнорируются
Есть ли способ прервать (Ctrl+C
) Python script на основе цикла, встроенного в расширение Cython?
У меня есть следующий python script:
def main():
# Intantiate simulator
sim = PySimulator()
sim.Run()
if __name__ == "__main__":
# Try to deal with Ctrl+C to abort the running simulation in terminal
# (Doesn't work...)
try:
sys.exit(main())
except (KeyboardInterrupt, SystemExit):
print '\n! Received keyboard interrupt, quitting threads.\n'
Запускает цикл, который является частью расширения С++ Cython.
Затем, нажимая Ctrl+C
, KeyboardInterrupt
вызывается, но игнорируется, и программа продолжает движение до конца моделирования.
Работа, которую я обнаружил, заключается в обработке исключения из внутреннего расширения, вылавливая сигнал SIGINT
:
#include <execinfo.h>
#include <signal.h>
static void handler(int sig)
{
// Catch exceptions
switch(sig)
{
case SIGABRT:
fputs("Caught SIGABRT: usually caused by an abort() or assert()\n", stderr);
break;
case SIGFPE:
fputs("Caught SIGFPE: arithmetic exception, such as divide by zero\n",
stderr);
break;
case SIGILL:
fputs("Caught SIGILL: illegal instruction\n", stderr);
break;
case SIGINT:
fputs("Caught SIGINT: interactive attention signal, probably a ctrl+c\n",
stderr);
break;
case SIGSEGV:
fputs("Caught SIGSEGV: segfault\n", stderr);
break;
case SIGTERM:
default:
fputs("Caught SIGTERM: a termination request was sent to the program\n",
stderr);
break;
}
exit(sig);
}
Тогда:
signal(SIGABRT, handler);
signal(SIGFPE, handler);
signal(SIGILL, handler);
signal(SIGINT, handler);
signal(SIGSEGV, handler);
signal(SIGTERM, handler);
Не могу ли я сделать эту работу с Python или, по крайней мере, с Cython? Поскольку я собираюсь переносить мое расширение под Windows/MinGW, я был бы признателен за то, что у него было меньше Linux.
Ответы
Ответ 1
Вы должны периодически проверять ожидающие сигналы, например, на каждой N-й итерации цикла моделирования:
from cpython.exc cimport PyErr_CheckSignals
cdef Run(self):
while True:
# do some work
PyErr_CheckSignals()
PyErr_CheckSignals
будет запускать обработчики сигналов, установленные с помощью signal (это включает в себя повышение KeyboardInterrupt
при необходимости).
PyErr_CheckSignals
довольно быстро, это нормально, чтобы часто его вызывать. Обратите внимание, что он должен вызываться из основного потока, потому что Python запускает обработчики сигналов в основном потоке. Вызов его из рабочих потоков не влияет.
Объяснение
Поскольку сигналы передаются асинхронно в непредсказуемое время, проблематично запускать любой значимый код непосредственно из обработчика сигнала. Поэтому Python ставит в очередь входящие сигналы. Очередь обрабатывается позже как часть цикла интерпретатора.
Если ваш код полностью скомпилирован, цикл интерпретатора никогда не выполняется, и Python не имеет возможности проверять и запускать обработчики обработанных посылок.
Ответ 2
Если вы пытаетесь обрабатывать KeyboardInterrupt
в коде, который выпускает GIL (например, потому что он использует cython.parallel.prange
), вам нужно будет повторно приобрести GIL для вызова PyErr_CheckSignals
. Следующий фрагмент (адаптированный из ответа @nikita-nemkin) показывает, что вам нужно сделать:
from cpython.exc cimport PyErr_CheckSignals
from cython.parallel import prange
cdef Run(self) nogil:
with nogil:
for i in prange(1000000)
# do some work but check for signals every once in a while
if i % 10000 == 0:
with gil:
PyErr_CheckSignals()
Ответ 3
Отпустите GIL, когда Cython запускает части, которые не взаимодействуют с Python, запускают цикл в основном потоке (спать или проверять состояние симуляции) и вызывают sim.Stop()
(который может установить некоторый флаг, который ваша симуляция может периодически проверять) except
закрыть.
Ответ 4
Да, используя макросы sig_on
и sig_off
из пакета cysignals
:
from cysignals.signals cimport sig_on, sig_off
def foo():
sig_on()
call_c_code_that_takes_long()
sig_off()
Макросы sig_on
и sig_off
объявлены как функции в cysignals/signals.pxd
и определены как макросы в cysignals/macros.h
в терминах макроса _sig_on_
(определяется в терминах функций _sig_on_prejmp
и _sig_on_postjmp
) и функция _sig_off_
. Обработчик сигналов для прерываний клавиатуры (SIGINT
) установлен здесь, и обоснование реализации описано здесь.
Как и в случае с cysignals == 1.6.5
, поддерживаются только системы POSIX. Cython условная компиляция может использоваться для того, чтобы следовать этому подходу, где бы cysignals
не был доступен, и разрешить компиляцию на не-POSIX-системах тоже (без Ctrl-C, работающий над этими системами).
В script setup.py
:
compile_time_env = dict(HAVE_CYSIGNALS=False)
# detect `cysignals`
if cysignals is not None:
compile_time_env['HAVE_CYSIGNALS'] = True
...
c = cythonize(...,
compile_time_env=compile_time_env)
и в соответствующем файле *.pyx
:
IF HAVE_CYSIGNALS:
from cysignals.signals cimport sig_on, sig_off
ELSE:
# for non-POSIX systems
noop = lambda: None
sig_on = noop
sig_off = noop
См. также этот ответ.