🚀 Python 并发实战:从“质数计算”看懂多进程的核心逻辑
🚀 Python 并发实战:从“质数计算”看懂多进程的核心逻辑
在后台开发中,我们常听到“并发”、“并行”、“异步”这些词。很多初学者容易混淆,或者知道概念但不知道什么时候该用、怎么用才安全。
今天,我们通过一个经典的**“大数质数判断”**案例,拆解 Python multiprocessing 的核心机制,帮你彻底搞懂并发编程的底层逻辑。
01. 什么是并发?为什么需要它?
想象一下,你是一家餐厅的主厨(主进程)。
- 串行(同步):客人点了一道菜,你做完这道,再做下一道。如果有一道菜需要炖煮 1 小时,其他客人都得等着。
- 并发(并行):你雇了 5 个帮厨(子进程)。当一道菜需要长时间炖煮时,你把它交给帮厨 A,同时自己继续处理其他订单,或者让帮厨 B、C 同时处理其他菜品。
并发的本质:利用多个执行单元(进程/线程),同时处理多个任务,从而缩短总耗时或提高吞吐量。
⏰ 什么时候必须用并发?
- CPU 密集型任务:如视频编码、复杂数学计算、图像渲染、机器学习推理。这类任务会占满 CPU,单核跑太慢,需要多核并行。
- IO 密集型任务:如大量网络请求、数据库查询、文件读写。虽然 Python 多线程更适合 IO,但多进程也能通过隔离避免 GIL 锁的影响,提高稳定性。
- 隔离性要求高:某个任务可能崩溃或内存泄漏,使用独立进程可以防止主程序挂掉。
💡 本案例场景:判断一个大数是否为质数,属于典型的 CPU 密集型计算,非常适合用多进程来加速。
02. 并发的前提:你需要什么?
并不是所有情况都适合开启并发。在启动多进程前,请确认以下前提:
- 硬件资源:你有足够的 CPU 核心吗?如果只有 1 核,多进程反而会因为上下文切换(Context Switch)导致更慢。
- 任务独立性:子任务之间是否共享大量状态?如果任务间需要频繁通信或共享内存,多进程的开销(序列化/反序列化)可能会抵消并行带来的收益。
- 操作系统支持:Windows 和 Linux/macOS 对进程创建的支持不同(见下文代码分析)。
03. 代码深度解析:一步步看穿多进程
让我们回顾一下你提供的代码,看看它是如何工作的。
🔧 核心组件
1 | import multiprocessing as mp |
1. 任务函数:纯逻辑,无副作用
1 | def is_prime(n): |
注意:
worker函数必须是可序列化的(即不能包含 lambda 或局部定义的复杂对象),因为它会被打包发送到子进程。
2. 启动方式的选择:跨平台兼容性
1 | if sys.platform.startswith("win"): |
这是很多初学者踩坑的地方!
fork(Linux/macOS 默认):直接复制父进程内存,速度极快,但不安全(可能复制锁状态)。spawn(Windows 唯一支持):启动一个全新的 Python 解释器,只导入必要的模块。安全但慢。forkserver(Linux 推荐):先启动一个服务器进程,后续子进程由它 fork 出来。兼顾速度与安全性。
3. 创建上下文与队列
1 | ctx = mp.get_context(start_method) |
使用 ctx.Queue() 而不是普通的 queue.Queue,因为后者是线程安全的,而多进程间通信需要基于管道或共享内存的特殊队列。
4. 启动进程与收集结果
1 | processes = [] |
最佳实践:始终调用
join()等待子进程结束,避免僵尸进程。
04. 并发的优缺点:一把双刃剑
✅ 优点
- 性能提升:充分利用多核 CPU,显著缩短 CPU 密集型任务的执行时间。
- 隔离性强:一个子进程崩溃不会影响主进程或其他子进程。
- 绕过 GIL:Python 的全局解释器锁(GIL)限制多线程并行执行字节码,但多进程每个进程有独立的 GIL,真正实现并行。
❌ 缺点
- 资源开销大:每个进程都有独立的内存空间,创建和销毁成本高。
- 通信复杂:进程间不能直接共享变量,必须通过 Queue、Pipe、Manager 等机制通信,涉及序列化(Pickle)开销。
- 调试困难:多进程日志分散,断点调试不如单进程直观。
- 数据一致性:如果需要共享状态,需引入锁或同步机制,容易引发死锁。
05. 总结与建议
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| CPU 密集计算 | Multiprocessing | 绕过 GIL,真正并行 |
| IO 密集(网络/DB) | Asyncio / Threading | 轻量级,切换成本低 |
| 简单脚本/小任务 | Single Thread | 避免过度工程化 |
🎯 给开发者的建议
- 先测量,再优化:不要盲目上并发。先用
time.time()或cProfile确认瓶颈是否在 CPU。 - 控制进程数:不要无限制创建进程。通常建议进程数 = CPU 核心数 + 1。可以使用
mp.cpu_count()获取。 - 善用连接池:如果是数据库或 HTTP 请求,考虑使用连接池而非每次新建进程。
- 日志清晰:像案例中一样,在日志中加入
pid,方便追踪问题来源。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Nosaw博客!
评论




