【问题标题】:Catch Ctrl+C / SIGINT and exit multiprocesses gracefully in python [duplicate]在python中捕获Ctrl + C / SIGINT并优雅地退出多进程[重复]
【发布时间】:2012-07-03 23:31:25
【问题描述】:

如何在多进程 python 程序中捕获 Ctrl+C 并优雅地退出所有进程,我需要该解决方案在 unix 和 windows 上都可以工作。我尝试了以下方法:

import multiprocessing
import time
import signal
import sys

jobs = []

def worker():
    signal.signal(signal.SIGINT, signal_handler)
    while(True):
        time.sleep(1.1234)
        print "Working..."

def signal_handler(signal, frame):
    print 'You pressed Ctrl+C!'
    # for p in jobs:
    #     p.terminate()
    sys.exit(0)

if __name__ == "__main__":
    for i in range(50):
        p = multiprocessing.Process(target=worker)
        jobs.append(p)
        p.start()

它确实有效,但我认为这不是正确的解决方案。

【问题讨论】:

    标签: python multiprocessing signals


    【解决方案1】:

    The previously accepted solution 具有竞争条件,它不适用于 mapasync 函数。


    Ctrl+C/SIGINTmultiprocessing.Pool的正确处理方式是:

    1. 在创建进程Pool 之前让进程忽略SIGINT。这样创建的子进程继承SIGINT处理程序。
    2. 在创建Pool 后,在父进程中恢复原来的SIGINT 处理程序。
    3. 使用map_asyncapply_async 而不是阻止mapapply
    4. 等待结果超时,因为默认阻塞等待忽略所有信号。这是 Python 错误https://bugs.python.org/issue8296

    把它放在一起:

    #!/bin/env python
    from __future__ import print_function
    
    import multiprocessing
    import os
    import signal
    import time
    
    def run_worker(delay):
        print("In a worker process", os.getpid())
        time.sleep(delay)
    
    def main():
        print("Initializng 2 workers")
        original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
        pool = multiprocessing.Pool(2)
        signal.signal(signal.SIGINT, original_sigint_handler)
        try:
            print("Starting 2 jobs of 5 seconds each")
            res = pool.map_async(run_worker, [5, 5])
            print("Waiting for results")
            res.get(60) # Without the timeout this blocking call ignores all signals.
        except KeyboardInterrupt:
            print("Caught KeyboardInterrupt, terminating workers")
            pool.terminate()
        else:
            print("Normal termination")
            pool.close()
        pool.join()
    
    if __name__ == "__main__":
        main()
    

    正如@YakovShklarov 所指出的,在父进程中忽略信号和取消忽略信号之间有一个时间窗口,在此期间信号可能会丢失。使用pthread_sigmask 代替在父进程中临时阻止信号的传递可以防止信号丢失,但是在 Python-2 中不可用。

    【讨论】:

    • 似乎你必须使用map_async,而不是map,任何人都可以暗示单一处理的区别吗? (似乎也没有必要在 map_async 结果上调用 .get)
    • 这不适用于 Windows 10 上的 Python 3.6.1,KeyboardInterrupt 未被捕获
    • @Boop 我不确定,需要调查一下。
    • 此解决方案不可移植,因为它仅适用于 Unix。此外,如果用户设置了maxtasksperchild Pool 参数,它将不起作用。新创建的进程将再次继承标准的SIGINT 处理程序。在创建新进程后,pebble 库默认为用户禁用 SIGINT
    • 请注意,阻塞调用问题已在 Python 3.3 中解决,您可以使用 map()apply()get() 而不会超时:bugs.python.org/issue9205
    【解决方案2】:

    该解决方案基于this linkthis link,它解决了问题,但我不得不搬到Pool

    import multiprocessing
    import time
    import signal
    import sys
    
    def init_worker():
        signal.signal(signal.SIGINT, signal.SIG_IGN)
    
    def worker():
        while(True):
            time.sleep(1.1234)
            print "Working..."
    
    if __name__ == "__main__":
        pool = multiprocessing.Pool(50, init_worker)
        try:
            for i in range(50):
                pool.apply_async(worker)
    
            time.sleep(10)
            pool.close()
            pool.join()
    
        except KeyboardInterrupt:
            print "Caught KeyboardInterrupt, terminating workers"
            pool.terminate()
            pool.join()
    

    【讨论】:

    • 这有点太晚了:在子进程中的fork() return 和signal() 调用之间有一个竞争条件窗口。分叉前必须阻塞信号。
    • @MaximYegorushkin - 信号在 init_worker 中被阻塞,在 apply_async 之前调用 - 你在说什么?
    • 这仅适用于 time.sleep。如果您尝试将get() 的结果改为map_async 调用的结果,则会延迟中断直到处理完成。
    • 这是一个错误的答案。正确答案:stackoverflow.com/a/35134329/412080
    • 当然可以。但这是错误的。来自文档:“每个工作进程在启动时都会调用 initializer(*initargs)。”那是“当”,而不是“之前”。所以:竞争条件。以下是可能发生的情况:创建了子进程,但在 signal.signal() 完成之前,发送了 SIGINT!子进程因未捕获的键盘中断而中止。这种情况很少见,但不能保证不会发生。 (实际上,如果你产生大量工人,这可能并不罕见。)如果你不阻止,可能发生的最糟糕的事情似乎只是你的终端上的垃圾。不过,这是不好的做法。
    【解决方案3】:

    只需在工作进程中处理 KeyboardInterrupt-SystemExit 异常:

    def worker():
        while(True):
            try:
                msg = self.msg_queue.get()
            except (KeyboardInterrupt, SystemExit):
                print("Exiting...")
                break
    

    【讨论】:

    • 对于使 Python 引发 SystemExit 的信号,这在 Python 3.6 上也确实有效。不过我想知道,这包括哪些信号?我猜是 SIGKILL 和 SIGTERM ...?
    • 您可以轻松检查包含哪些信号,答案是:我认为没有。 SystemExit 仅由 sys.exit 根据文档提出。只需执行try: time.sleep(60) except BaseException as e: print(e),您就会看到是否捕获到特定信号(ime only SIGINT)。手册页也是这么说的。
    • @Petri 可能只是信号情报。我相信 SIGKILL 是无法捕获的,而 SIGTERM 是另外一回事。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-22
    • 1970-01-01
    • 2011-01-27
    • 1970-01-01
    • 2012-01-07
    相关资源
    最近更新 更多