Python 多进程中的 KeyboardInterrupt 异常

3
我希望编写一个服务,启动多个无限工作进程,并在主进程接收到 Ctrl+C 信号时终止这些子进程。不过我不知道如何正确地处理这个信号。
以下是我测试使用的代码:
import os
import multiprocessing as mp
    

def g():
    print(os.getpid())
    while True:
        pass
        
        
def main():
    with mp.Pool(1) as pool:
        try:
            s = pool.starmap(g, [[]] * 1)
        except KeyboardInterrupt:
            print('Done')


if __name__ == "__main__":
    print(os.getpid())
    main()


当我尝试使用Ctrl+C时,我期望正在运行g的进程只接收SIGTERM并静默终止,但是我实际上收到了如下输出:
Process ForkPoolWorker-1:
Done
Traceback (most recent call last):
  File "/usr/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
  File "/usr/lib/python3.8/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/lib/python3.8/multiprocessing/pool.py", line 125, in worker
    result = (True, func(*args, **kwds))
  File "/usr/lib/python3.8/multiprocessing/pool.py", line 51, in starmapstar
    return list(itertools.starmap(args[0], args[1]))
  File "test.py", line 8, in g
    pass
KeyboardInterrupt

这显然意味着父进程和子进程都会从Ctrl+C中抛出KeyboardInterrupt,这得到了使用kill -2测试的进一步证实。为什么会发生这种情况,如何处理才能达到我想要的效果?


这个回答解决了你的问题吗?Python多进程池中的键盘中断 - Charchit Agarwal
@Charchit 不,你链接的问题涉及旧版Python中的错误,但那个问题已经被修复了,我的问题与此无关。 - steam_engine
被接受的答案谈到了这个 bug,其他答案更为新近,而这个(在同一线程上的)答案基本上与此处被接受的答案说的是一样的。 - Charchit Agarwal
1个回答

3
< p > 触发 KeyboardInterrupt 信号将传递给整个进程池。子进程对待它的方式与父进程相同,都会引发 KeyboardInterrupt 异常。

最简单的解决方案是:

  1. 在每个进程创建时禁用 SIGINT 处理
  2. 确保当父进程捕获到 KeyboardInterrupt 异常时终止所有子进程

您可以通过传递一个 initializer 函数来实现这一点,Pool 在每个工作进程开始工作之前运行该函数:

import signal
import multiprocessing as mp

# Use initializer to ignore SIGINT in child processes
with mp.Pool(1, initializer=signal.signal, initargs=(signal.SIGINT, signal.SIG_IGN)) as pool:
    try:
        s = pool.starmap(g, [[]] * 1)
    except KeyboardInterrupt:
        print('Done')

initializer替换默认的SIGINT处理程序,使用一个忽略子进程中的SIGINT信号的处理程序,由父进程负责杀死子进程。在父进程中,with语句会自动处理这个问题(退出with块会隐式地调用pool.terminate()),所以你需要做的就是在父进程中捕获KeyboardInterrupt,并将其从丑陋的回溯转换成简单的消息。


使用初始化程序来忽略 SIGINT 是优雅的,我很可能会使用它。但为什么它会传递到整个进程池呢?当我使用 kill -2 <parent pid> 进行测试时,它只是按照我的预期进行操作,只将信号发送给父进程并打印“完成”。这是 bash 的行为吗? - steam_engine
3
根据这个答案,“SIGINT信号由终端行规程生成,并广播到终端的前台进程组中的所有进程。您的shell已经为运行的命令(或命令管道)创建了一个新的进程组,并告诉终端该进程组是其(终端的)前台进程组。” 基本上,在 POSIX 上,按下Ctrl-C不仅会发送SIGINT给父进程,而且会发送给整个前台进程组。 - ShadowRanger
可能 os.setpgrp() 也可以(未经测试,请勿引用),如果子进程 initializer 使用它来从父进程组分离,但这仅适用于UNIX,因此如果您想要跨操作系统获得一致的行为,则不可行。 - ShadowRanger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接