忽略Python多处理子流程中的SIGINT

时间:2017-03-29 11:36:09

标签: python windows multiprocessing signals

当我在OSX或Linux上运行以下代码然后按ctrl+c时,将启动“正常关闭”。看起来像这样:

$ python subprocess_test.py    
Subprocess:  <MyProcess(MyProcess-1, started)>
^CMain: Graceful shutdown
Subprocess: shutdown

但是,当我在Windows10计算机上运行某些代码时,行KeyboardInterrupt中会引发self.event.wait(),导致正常关闭。我已经尝试了here描述的不同方法来防止子进程正在接收信号。

使用Python 2.7在不同操作系统上获得相同行为的正确方法是什么?

import multiprocessing
import signal

class MyProcess(multiprocessing.Process):

    def __init__(self):
        super(MyProcess, self).__init__()
        self.event = multiprocessing.Event()

    def run(self):
        print "Subprocess: ", multiprocessing.current_process()
        self.event.wait()
        print "Subprocess: shutdown"


def sighandler(a,b,):
    print "Main: Graceful shutdown"
    p1.event.set()

def run():
    signal.signal(signal.SIGINT, signal.SIG_IGN)
    global p1
    p1 = MyProcess()

    p1.start()

    signal.signal(signal.SIGINT, sighandler)
    p1.join()


if __name__ == '__main__':
    run()

2 个答案:

答案 0 :(得分:1)

在Windows SIGINT上使用CTRL_C_EVENT的控制台控件事件处理程序实现。它是由子进程继承的控制台状态,而不是CRT的信号处理状态。因此,如果您希望孩子忽略Ctrl + C,则需要先在父进程中调用SetConsoleCtrlHandler忽略Ctrl + C,然后再创建子进程。

有一个问题。 Python不在Windows上使用可警告的等待,例如在进程join方法中等待。由于它在主线程上调度信号处理程序,因此主线程在join()中被阻塞的事实意味着永远不会调用您的信号处理程序。您必须使用time.sleep()上的循环替换连接,这可以通过Ctrl + C进行中断,因为它在内部等待Windows事件并设置自己的控制处理程序来设置此事件。或者您可以通过ctypes使用自己的异步控制处理程序集。以下示例实现了这两种方法,并在Python 2和3中都有效。

import sys
import signal
import multiprocessing

if sys.platform == "win32":
    # Handle Ctrl+C in the Windows Console
    import time
    import errno
    import ctypes
    import threading

    from ctypes import wintypes

    kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)

    PHANDLER_ROUTINE = ctypes.WINFUNCTYPE(
        wintypes.BOOL,
        wintypes.DWORD) # _In_ dwCtrlType

    win_ignore_ctrl_c = PHANDLER_ROUTINE() # alias for NULL handler

    def _errcheck_bool(result, func, args):
        if not result:
            raise ctypes.WinError(ctypes.get_last_error())
        return args

    kernel32.SetConsoleCtrlHandler.errcheck = _errcheck_bool
    kernel32.SetConsoleCtrlHandler.argtypes = (
        PHANDLER_ROUTINE, # _In_opt_ HandlerRoutine
        wintypes.BOOL)    # _In_     Add

class MyProcess(multiprocessing.Process):

    def __init__(self):
        super(MyProcess, self).__init__()
        self.event = multiprocessing.Event()

    def run(self):
        print("Subprocess: %r" % multiprocessing.current_process())
        self.event.wait()
        print("Subprocess: shutdown")

    if sys.platform == "win32":
        def join(self, timeout=None):
            if threading.current_thread().name != "MainThread":
                super(MyProcess, self).join(timeout)
            else:
                # use time.sleep to allow the main thread to
                # interruptible by Ctrl+C
                interval = 1
                remaining = timeout
                while self.is_alive():
                    if timeout is not None:
                        if remaining <= 0:
                            break
                        if remaining < interval:
                            interval = remaining
                            remaining = 0
                        else:
                            remaining -= interval
                    try:
                        time.sleep(interval)
                    except IOError as e:
                        if e.errno != errno.EINTR:
                            raise
                        break

def run():
    p1 = MyProcess()

    # Ignore Ctrl+C, which is inherited by the child process.
    if sys.platform == "win32":
         kernel32.SetConsoleCtrlHandler(win_ignore_ctrl_c, True)
    signal.signal(signal.SIGINT, signal.SIG_IGN)

    p1.start()

    # Set a Ctrl+C handler to signal graceful shutdown.
    if sys.platform == "win32":
        kernel32.SetConsoleCtrlHandler(win_ignore_ctrl_c, False)
        # comment out the following to rely on sig_handler
        # instead. Note that using the normal sig_handler requires
        # joining using a loop on time.sleep() instead of the
        # normal process join method. See the join() method
        # defined above.
        @PHANDLER_ROUTINE
        def win_ctrl_handler(dwCtrlType):
            if (dwCtrlType == signal.CTRL_C_EVENT and
                not p1.event.is_set()):
                print("Main <win_ctrl_handler>: Graceful shutdown")
                p1.event.set()
            return False

        kernel32.SetConsoleCtrlHandler(win_ctrl_handler, True)

    def sig_handler(signum, frame):
        if not p1.event.is_set():
            print("Main <sig_handler>: Graceful shutdown")
            p1.event.set()

    signal.signal(signal.SIGINT, sig_handler)
    p1.join()

if __name__ == "__main__":
    run()

答案 1 :(得分:1)

使用pywin32中的win32api.SetConsoleCtrlHandler可以控制Windows的中断方式。使用SetConsoleCtrlHandler(None, True)会导致调用进程忽略CTRL + C输入。使用SetConsoleCtrlHandler(sighandler, True)可以注册特定的处理程序。

将所有问题全部解决,问题如下:

import multiprocessing
import signal
import sys

class MyProcess(multiprocessing.Process):

    def __init__(self):
        super(MyProcess, self).__init__()
        self.event = multiprocessing.Event()

    def run(self):
        if sys.platform == "win32":
            import win32api # ignoring the signal
            win32api.SetConsoleCtrlHandler(None, True)

        print "Subprocess: ", multiprocessing.current_process()
        self.event.wait()
        print "Subprocess: shutdown"


def sighandler(a,b=None):
    print "Main: Graceful shutdown"
    p1.event.set()

def run():
    signal.signal(signal.SIGINT, signal.SIG_IGN)

    global p1
    p1 = MyProcess()

    p1.start()

    if sys.platform == "win32":
        import win32api
        win32api.SetConsoleCtrlHandler(sighandler, True)
    else:
        signal.signal(signal.SIGINT, sighandler)

    p1.join()


if __name__ == '__main__':
    run()