Python 多进程与进程池
多进程绕过 GIL 做 CPU 并行;注意序列化、内存与启动方式,优先进程池管理生命周期。
#concept
#growing
#dev
#python
#python
[!info] related notes
Python 多进程与进程池
这个概念为什么出现
在启用 GIL 的 CPython 中,纯 Python CPU 循环用线程通常难以多核并行。进程提供独立解释器,但需承担序列化、启动和内存成本;压缩、哈希、图像库可能在原生层释放 GIL,不能只按任务名称判断。
[!abstract] 一句话理解 进程池把 CPU 任务分到多进程执行以绕过 GIL;参数/返回值需可 pickle,适合可分割的计算型负载。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:将四组纯 Python 累加任务交给进程池
# process_pool_hash.py
# 示例意图:分发纯 Python 累加任务;不计算文件哈希或校验和。
# 教学点:
# - ProcessPoolExecutor;
# - 顶层可 pickle 函数;
# - if __name__ 保护(spawn/forkserver)。
from concurrent.futures import ProcessPoolExecutor
from time import perf_counter
def crunch(n: int) -> int:
# 模拟 CPU:累加
total = 0
for i in range(n):
total += i % 7
return total
def main() -> None:
work = [300000] * 4
t0 = perf_counter()
with ProcessPoolExecutor(max_workers=4) as pool:
results = list(pool.map(crunch, work))
print("results:", results)
print("elapsed≈", round(perf_counter() - t0, 3))
if __name__ == "__main__":
main()
建议运行:
python process_pool_hash.py
期望输出(elapsed 随机器变化):
results: [899997, 899997, 899997, 899997]
elapsed≈ 0.xxx
结合场景再看三个关注点
- 函数与参数/结果需可 pickle,工作函数通常应定义在可导入模块顶层;REPL/Notebook 中的
__main__不一定可被子进程导入。 if __name__ == "__main__"防止 spawn 递归。- 任务太小会被进程开销吃掉收益。
核心概念与准确模型
multiprocessing/ProcessPoolExecutor- 启动方式与平台/版本有关:Windows/macOS 通常用 spawn;3.14 在支持 forkserver 的 POSIX 平台默认改为 forkserver,fork 不再是任何平台的默认方式。需要特定语义时显式传 mp_context。
- 队列、共享内存进阶 API
- 与线程池接口相似(futures)
边界情况与反直觉行为
- lambda/闭包难 pickle。
- 大对象传输成为瓶颈。
- 全局可变状态不因子进程启动就实时共享;fork 的初始内存快照也不是共享更新,需显式 IPC/共享内存。
- Future.result(timeout=…) 只限制等待,不停止运行中的进程任务;cancel 通常只能取消尚未开始的任务,退出池上下文默认仍等待任务结束。
常见误区
[!warning] 常见误区:所有变慢都上多进程 错误理解:进程越多越快。
正确模型:先确认 CPU 瓶颈与任务粒度。
工程实践
- 池大小≈CPU 核数(容器限额内)
- 批处理合并小任务
- 失败重试与超时
本节总结
多进程是 CPython CPU 并行主路径之一;把它当分布式计算的缩小版管理。
自测题
- 为什么需要
if __name__保护? - 为何小任务可能不加速?
参考答案
- spawn 会再导入主模块,无保护会重复启动。
- 进程启动与序列化开销大于计算本身。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| multiprocessing | 文档 | 进程 |
| ProcessPoolExecutor | 文档 | 进程池 |