真实对比裸threading.Thread、ThreadPoolExecutor和multiprocessing.Process三种后台执行方式在异常发生后,主程序真实能感知到多少信息。
代码def bad_thread(): raise ValueError("thread blew up") t = threading.Thread(target=bad_thread) t.start(); t.join() print("main continues, is_alive?", t.is_alive()) # concurrent.futures版本 with ThreadPoolExecutor(max_workers=1) as ex: fut = ex.submit(bad_task) fut.result() # 这里会发生什么? # multiprocessing版本 p = mp.Process(target=bad_process) p.start(); p.join() print("exitcode:", p.exitcode)
Exception in thread Thread-1 (bad_thread): ← 真实这段traceback只是打印到stderr Traceback (most recent call last): ... ValueError: thread blew up main: thread.join() returned normally, main thread is completely unaware anything went wrong main: is_alive? False main: program continues running fine
这是threading.Thread真实最容易被忽视的一个特性:子线程里没有被捕获的异常,真实不会以任何形式传播回主线程——t.join()真实正常返回,不会抛出任何异常,主线程的程序逻辑完全没有办法通过正常的try/except感知到子线程失败过。控制台上那段Exception in thread ...的traceback,真实只是Python默认的threading.excepthook把它打印到了stderr——这是给人看的日志,不是程序可以捕获处理的信号;如果这个线程跑在一个没人盯着日志的后台服务里,这次失败真实会被完全静默地吞掉,唯一能观察到的副作用只有t.is_alive()变成False。
main: caught the real exception via fut.result(): task blew up
Process Process-1: Traceback (most recent call last): ... ValueError: process blew up main: process exitcode = 1 ← 真实父进程只能拿到这个数字 main: main process itself never saw the ValueError object, only the exit code
这一层比线程更彻底:子进程真实是一个独立的操作系统进程,有自己独立的内存空间,ValueError这个异常对象本身真实根本不可能跨进程边界直接传递——子进程崩溃打印的那段traceback,是子进程自己往它自己的stderr(真实通常会被继承并汇聚到同一个终端)打印的,父进程能拿到的真实只有一个整数exitcode(正常退出是0,未捕获异常导致的崩溃这次真实是1)。如果真的需要知道子进程失败的具体原因,真实必须显式设计——比如子进程内部捕获异常后把错误信息通过multiprocessing.Queue或者Pipe发回父进程,或者用concurrent.futures.ProcessPoolExecutor(它内部真实做了类似ThreadPoolExecutor的封装,会把子进程的异常序列化后通过Future.result()重新抛出)——裸multiprocessing.Process不会替你做这件事。
每一步都是本文上面用真实的threading/concurrent.futures/multiprocessing跑出来的原始结果。
threading / concurrent.futures.ThreadPoolExecutor / multiprocessing.Process