在python中捕获Ctrl + C / SIGINT并优雅地退出多进程[重复]

Posted

技术标签:

【中文标题】在python中捕获Ctrl + C / SIGINT并优雅地退出多进程[重复]【英文标题】:Catch Ctrl+C / SIGINT and exit multiprocesses gracefully in python [duplicate] 【发布时间】: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()

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

【问题讨论】:

【参考方案1】:

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


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

    在创建进程Pool 之前让进程忽略SIGINT。这样创建的子进程继承SIGINT处理程序。 在创建Pool 后,在父进程中恢复原来的SIGINT 处理程序。 使用map_asyncapply_async 而不是阻止mapapply。 等待结果超时,因为默认阻塞等待忽略所有信号。这是 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 link 和this 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 调用的结果,则会延迟中断直到处理完成。 这是一个错误的答案。正确答案:***.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 是另外一回事。

以上是关于在python中捕获Ctrl + C / SIGINT并优雅地退出多进程[重复]的主要内容,如果未能解决你的问题,请参考以下文章

cmd模块:捕获Ctrl + C [重复]

使用 Spyder 3.3 无法在 input() 中捕获 Ctrl+C

[python]捕获ctrl+C事件后处理

如何使用 PyQt 在 python 应用程序中处理 Ctrl+C?

在C程序中捕获Ctrl + C

python的异常处理