Python 30:多进程多出的成本在哪里
把同一段 "One two one" 和同样的 cpu_work(..., iterations=20000) 交给顺序入口与进程池入口,两者的任务结果都是 {"one": 2, "two": 1}。进程池回传的工作者 PID 与父进程不同,子进程的解释器路径仍是 /usr/bin/python3.12。但把本地 lambda 当参数送进进程池时,结果 Future 报 Can't pickle local object;父任务被记为 failed,没有成功结果。多进程能并行运行独立解释器,并不意味着它能共享任意 Python 函数或当前进程里的对象。
29 的线程池通过共享同一个进程里的任务对象协作;本篇只添加进程池执行策略,没有更换提交、执行、查询、取消的业务规则。进程启动方式明确选 spawn,不能把当前 Linux 上别的默认设置推广给所有平台。
明确送过去的是什么
ProcessTaskRunner 用 ProcessPoolExecutor(mp_context=multiprocessing.get_context("spawn"));工作者入口 _execute 位于可导入模块的顶层,接收文本与迭代次数,在子进程新建自己的内存 store,通过同一个 app.submit → app.run → app.query 执行词频任务。工作者初始化函数记录自己的 PID;实际返回 PID、解释器、状态和计算结果,以便检查任务真的由另一个进程完成。子进程不接收父进程的 SQLite 连接,也不把父进程字典当成共享对象。
父进程在已有的 app.run 钩子里等待进程 Future 的结果;此时父任务状态为 running。收到子进程结果后,用本应用的 count_words(iter_words(...)) 核对同一文本的结果,再让父任务执行原来的提交路径,写入 succeeded。这会为核对多做一次轻量词频计算,不能把时间差直接解读为“进程计算更慢”;spawn 的解释器启动、参数与结果的序列化、池初始化和数据复制也都包含在策略成本中。本篇没有声称测出了通用的加速比,36 才在冻结输入与多次样本下比较。
进程池对传入参数需要序列化。lambda 定义在本地,不能作为这个例子里的可导入顶层对象被 pickle;实际报错发生在 Future.result() 路径,不是说 executor.submit 返回 Future 本身立即抛错。测试还把不可序列化的迭代次数交给 ProcessTaskRunner.run,观察业务层 TaskExecutionError 的 __cause__ 是序列化错误,父任务为 failed、result 为 None。本次最初的测试代码错误地要求外层异常消息包含“pickl”,导致两项局部测试中一项失败;失败日志原样保存,修复断言去检查异常链后重新执行全套。没有为让测试变绿而吞掉业务层包装异常。
1 | |
实验入口必须在 if __name__ == "__main__" 下调用,以免 spawn 子进程重新导入主模块时反复创建进程池。累计测试结果、工作者 PID、解释器路径、异常链及页面证据保存在 examples/python-language/evidence/30/20261007-process/;首次失败保存在并列的 20261007-process-failed/。本章只跑正常退出的进程池和参数序列化故障,不把 shutdown(wait=True) 说成强制终止后的 Python finally 已执行。强杀与恢复必须在 35 单独验收。
预测题
把父进程的 SQLiteTaskGateway 连接或闭包里的本地 lambda 作为 spawn 工作函数参数传入,为什么不能按“线程里能引用,所以进程里也能引用”推断?
答案:线程共用地址空间;spawn 子进程需通过可序列化边界取得工作函数与参数。这个实验实际验证了本地 lambda 的失败,不能因此断言所有可能的连接对象都已测过;设计上应让子进程按需要独立初始化资源,而非把父连接当作跨进程共享。父业务任务必须在子任务结果确认后按原状态合同提交。
变更题
将 ProcessTaskRunner 中 mp_context 删除,只在当前 Linux 机器重跑通过,能否推出 Windows 与 macOS 会得到相同启动语义?
答案:不能。各平台及 Python 版本的默认启动方式可能不同;不固定方式就连本章的“子进程从可导入模块初始化”前提也没有冻结。继续固定 spawn 并在真实目标平台重新运行;本次只在记录的 Linux/CPython 3.12.3 执行,其余平台未测。不要从 wheel 的平台无关标签推导运行时进程行为。
31 用 asyncio 复用同样任务结果,区分创建协程、调度 Task 和运行纯 Python CPU 工作。系列导航只链接实际生成的内容。



