You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python多进程中Ctrl-C的工作机制及异常捕获问题咨询

ProcessPoolExecutor中Ctrl-C异常处理问题的解决方案

问题重现

使用ProcessPoolExecutor分发任务时,子进程的常规异常可通过future.result()正常捕获处理,但Ctrl-C触发的KeyboardInterrupt存在以下问题:

  • 按下Ctrl-C时,KeyboardInterrupt会被多次抛出,重复打印中断提示
  • 移除专门的KeyboardInterrupt捕获分支后,通用except Exception无法捕获Ctrl-C异常
  • 该问题在Linux和Windows系统均存在

原代码如下:

import time
from concurrent.futures import ProcessPoolExecutor, Future, wait
import traceback


def worker():
    time.sleep(5)
    # raise RuntimeError("Some Error")
    return True


def main():
    with ProcessPoolExecutor() as executor:
        stop = False
        tasks = set()

        while True:
            try:
                # keep submitting new tasks
                if not stop:
                    time.sleep(0.1)
                    print("Submitting task to worker")
                    future = executor.submit(worker)
                    tasks.add(future)

                done, not_done = wait(tasks, timeout=0)
                tasks = not_done

                # get the result
                for future in done:
                    try:
                        result = future.result()
                    except Exception as e:
                        print(f"Exception in worker: {type(e).__name__}: {e}")
                    else:
                        print(f"Worker finished with result: {result}")

                # exit loop if there are no tasks left and loop was stopped
                if stop:
                    print(f"Waiting for {len(tasks)} to finish.")
                    if not len(tasks):
                        print("Finished all remaining tasks")
                        break
                    time.sleep(1)
            except KeyboardInterrupt:
                print("Recieved Ctrl-C")
                stop = True
            except Exception as e:
                print(f"Caught {e}")
                stop = True


if __name__ == "__main__":
    main()

原因分析

  1. 多次抛出KeyboardInterrupt:主进程的while循环在每次迭代时,操作系统会将SIGINT信号(Ctrl-C)传递给主进程,导致每次循环都触发KeyboardInterrupt异常,重复执行捕获分支代码。
  2. 通用Exception无法捕获Ctrl-C:KeyboardInterrupt继承自BaseException而非Exception,因此except Exception分支不会捕获该异常,必须直接捕获KeyboardInterrupt或使用except BaseException。

解决方案

跨平台适配方案(Linux/Windows通用)

通过信号处理函数捕获SIGINT,设置全局中断标志,避免异常多次抛出;同时确保正确处理KeyboardInterrupt的继承关系。

修改后的代码:

import time
import signal
from concurrent.futures import ProcessPoolExecutor, wait

# 全局中断标志
interrupted = False

def handle_sigint(signum, frame):
    """处理SIGINT信号,仅触发一次中断提示"""
    global interrupted
    if not interrupted:
        print("Received Ctrl-C")
        interrupted = True

def worker():
    time.sleep(5)
    # raise RuntimeError("Some Error")
    return True

def main():
    # 注册SIGINT信号处理器
    signal.signal(signal.SIGINT, handle_sigint)
    
    with ProcessPoolExecutor() as executor:
        tasks = set()

        while True:
            if interrupted:
                # 停止提交新任务,等待现有任务完成
                print(f"Waiting for {len(tasks)} tasks to finish.")
                if not tasks:
                    print("Finished all remaining tasks")
                    break
                time.sleep(1)
            else:
                # 持续提交新任务
                time.sleep(0.1)
                print("Submitting task to worker")
                future = executor.submit(worker)
                tasks.add(future)

            # 处理完成的任务
            done, not_done = wait(tasks, timeout=0)
            tasks = not_done

            for future in done:
                try:
                    result = future.result()
                except Exception as e:
                    print(f"Exception in worker: {type(e).__name__}: {e}")
                else:
                    print(f"Worker finished with result: {result}")

if __name__ == "__main__":
    main()

关键优化点

  • 使用signal模块注册SIGINT处理器,确保Ctrl-C仅触发一次中断提示,避免重复抛出异常
  • 用全局标志interrupted控制循环流程,替代原异常捕获逻辑,简化代码结构
  • 保留子进程异常的原有处理逻辑,确保常规异常仍能正常捕获

Linux优先优化(可选)

如果仅需支持Linux,可在捕获SIGINT后临时屏蔽信号,彻底避免后续触发:

def handle_sigint(signum, frame):
    global interrupted
    if not interrupted:
        print("Received Ctrl-C")
        interrupted = True
        # Linux下屏蔽后续SIGINT信号
        signal.signal(signal.SIGINT, signal.SIG_IGN)

验证效果

  • 按下Ctrl-C后,仅会打印一次"Received Ctrl-C"
  • 主进程会等待所有已提交的子进程任务完成后退出
  • 子进程抛出的常规异常仍可通过future.result()正常捕获处理

内容的提问来源于stack exchange,提问作者BlackCatCoder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 21:47:34