在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 具有竞争条件,它不适用于 map
和 async
函数。
Ctrl+C/SIGINT
与multiprocessing.Pool
的正确处理方式是:
-
在创建进程
Pool
之前让进程忽略SIGINT
。这样创建的子进程继承SIGINT
处理程序。
在创建Pool
后,在父进程中恢复原来的SIGINT
处理程序。
使用map_async
和apply_async
而不是阻止map
和apply
。
等待结果超时,因为默认阻塞等待忽略所有信号。这是 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并优雅地退出多进程[重复]的主要内容,如果未能解决你的问题,请参考以下文章
使用 Spyder 3.3 无法在 input() 中捕获 Ctrl+C