Cython, Python and KeyboardInterrupt ignored

前端 未结 4 1947
太阳男子
太阳男子 2020-12-05 11:19

Is there a way to interrupt (Ctrl+C) a Python script based on a loop that is embedded in a Cython extension?

I have the following python script:

相关标签:
4条回答
  • 2020-12-05 12:05

    You have to periodically check for pending signals, for example, on every Nth iteration of the simulation loop:

    from cpython.exc cimport PyErr_CheckSignals
    
    cdef Run(self):
        while True:
            # do some work
            PyErr_CheckSignals()
    

    PyErr_CheckSignals will run signal handlers installed with signal module (this includes raising KeyboardInterrupt if necessary).

    PyErr_CheckSignals is pretty fast, it's OK to call it often. Note that it should be called from the main thread, because Python runs signal handlers in the main thread. Calling it from worker threads has no effect.

    Explanation

    Since signals are delivered asynchronously at unpredictable times, it is problematic to run any meaningful code directly from the signal handler. Therefore, Python queues incoming signals. The queue is processed later as part of the interpreter loop.

    If your code is fully compiled, interpreter loop is never executed and Python has no chance to check and run queued signal handlers.

    0 讨论(0)
  • 2020-12-05 12:17

    Yes, using the macros sig_on and sig_off from the package cysignals:

    from cysignals.signals cimport sig_on, sig_off
    
    def foo():
        sig_on()
        call_c_code_that_takes_long()
        sig_off()
    

    The macros sig_on and sig_off are declared as functions in cysignals/signals.pxd, and defined as macros in cysignals/macros.h in terms of the macro _sig_on_ (defined in terms of the functions _sig_on_prejmp and _sig_on_postjmp) and the function _sig_off_. The signal handler for keyboard interrupts (SIGINT) is installed here, and the implementation rationale is outlined here.

    As of cysignals == 1.6.5, only POSIX systems are supported. Cython's conditional compilation can be used in order to follow this approach wherever cysignals is available, and allow compiling on non-POSIX systems too (without Ctrl-C working on those systems).

    In the 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)
    

    and in the relevant *.pyx file:

    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
    

    See also this answer.

    0 讨论(0)
  • 2020-12-05 12:18

    If you are trying to handle KeyboardInterrupt in code that releases the GIL (for example, because it uses cython.parallel.prange), you will need to re-acquire the GIL to call PyErr_CheckSignals. The following snippet (adapted from @nikita-nemkin's answer above) illustrates what you need to do:

    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()
    
    0 讨论(0)
  • 2020-12-05 12:18

    Release GIL when Cython runs parts that do not interface with Python, run loop in the main thread (sleep or check the simulation status), and call sim.Stop() (that can set some flag that your simulation can check periodically) in the except close.

    0 讨论(0)
提交回复
热议问题