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

См. также этот ответ.