Cython、Python 和 KeyboardInterrupt 被忽略。

15
有没有一种方法可以基于嵌入在Cython扩展中的循环来中断(Ctrl+C)Python脚本?
我有以下Python脚本:
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'

这段代码是C++ 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的东西。

4个回答

16

你需要定期检查待处理信号,例如,在模拟循环的每第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没有机会检查和运行排队的信号处理程序。


PyErr_CheckSignals() 在 Windows 上能否处理 Ctrl-C 信号?我选择在主线程中使用 while sim.running: time.sleep(1) 循环来避免隐式信号检查,只要另一个线程中的 Cython 代码定期释放 GIL 就可以工作。+1 for the explanation. - jfs
我错误地使用了“opt out”。我本意是相反的:“s/opt out/choose to do/”在上面。 - jfs
谢谢。我会试一试,并且仍然想对其进行基准测试,因为在我的模拟中性能非常重要(可能需要数小时,甚至数天)。但这似乎是我一直梦寐以求的解决方案... - Gauthier Boaglio
这对于任何在Cython中编写的长时间运行的循环都是必需的,还是只针对调用外部C函数的循环? - Snorfalorpagus

3

如果您正在处理释放GIL的代码中的KeyboardInterrupt(例如,因为它使用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()

1
除非你在主线程中,否则调用PyErr_CheckSignals是毫无意义的。 - jfs

1
当Cython运行与Python无关的部分时,释放GIL,在主线程中运行循环(睡眠或检查模拟状态),并在except中调用sim.Stop()(可以设置一些标志,供您的模拟定期检查)。

好的,那听起来很有意义(即使我暗自希望有一个不费力的解决方案;-)。谢谢你的建议。 - Gauthier Boaglio

0

是的,可以使用来自cysignals包的宏sig_onsig_off

from cysignals.signals cimport sig_on, sig_off

def foo():
    sig_on()
    call_c_code_that_takes_long()
    sig_off()

sig_onsig_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 不起作用)。

在脚本 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

还请参阅此答案


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