多进程 multiprocessing
约 1504 字大约 5 分钟
2026-05-10
进程是操作系统分配资源的基本单位。每个进程有独立的内存空间。Python 的多进程可以绕过 GIL,让 CPU 密集型任务真正利用多核。
- 进程与线程
- 创建进程
- 为什么需要 main 保护
- 进程间内存隔离
- 1所有 Process(target=...) 启动代码必须写在 if __name__ == '__main__': 下面,否则 spawn 平台会无限递归。
- 2在子进程里改 global 变量再回主进程看:值没变 —— 体会进程间内存隔离。
- 3用 multiprocessing.Queue(不是 queue.Queue)在主子进程之间传数据。
- 4对比线程 vs 进程跑同一段大数计算:进程能并行,线程被 GIL 拖死。
- 5实际项目几乎不会手写 Process —— 用 ProcessPoolExecutor 更简单也更安全。
进程是操作系统分配资源的基本单位。每个进程有独立的内存空间。Python 的多进程可以绕过 GIL,让 CPU 密集型任务真正利用多核。
进程与线程
| 对比 | 线程 | 进程 |
|---|---|---|
| 内存 | 同一进程内共享内存 | 默认内存隔离 |
| 创建成本 | 较低 | 较高 |
| 通信 | 共享变量、队列 | 队列、管道、共享内存 |
| 适合场景 | IO 密集型 | CPU 密集型 |
| GIL 影响 | 受影响 | 不同进程各有 GIL |
创建进程
from multiprocessing import Process
def worker(name):
print(f'子进程执行:{name}')
if __name__ == '__main__':
process = Process(target=worker, args=('task-1',))
process.start()
process.join()
print('子进程结束')start() 启动进程,join() 等待进程结束。
为什么需要 main 保护
在 Windows 和 macOS 的 spawn 模式下,子进程会重新导入主模块。如果没有:
if __name__ == '__main__':子进程导入模块时可能再次创建子进程,导致无限递归。
因此,写 multiprocessing 时应养成习惯:进程启动代码放到 if __name__ == '__main__': 下面。
进程间内存隔离
from multiprocessing import Process
count = 0
def add():
global count
count += 1
print(f'子进程 count={count}')
if __name__ == '__main__':
p = Process(target=add)
p.start()
p.join()
print(f'主进程 count={count}')子进程修改的是自己的内存副本,不会直接修改主进程中的 count。
进程间通信
可以使用 multiprocessing.Queue 在进程间传递数据。
from multiprocessing import Process, Queue
def worker(q):
q.put('hello from child')
if __name__ == '__main__':
q = Queue()
p = Process(target=worker, args=(q,))
p.start()
print(q.get())
p.join()注意这里使用的是 multiprocessing.Queue,不是 queue.Queue。
CPU 密集型示例
from multiprocessing import Process
def calculate(n):
total = 0
for i in range(n):
total += i * i
print(total)
if __name__ == '__main__':
processes = []
for _ in range(4):
p = Process(target=calculate, args=(10_000_000,))
p.start()
processes.append(p)
for p in processes:
p.join()这类计算任务适合多进程,因为每个进程可以在不同 CPU 核心上运行。
注意事项
- 多进程创建成本比线程高,不适合大量短小任务频繁创建销毁。
- 进程间数据传递需要序列化,传递大对象成本较高。
- 在 macOS 和 Windows 上,必须重视
if __name__ == '__main__'。 - 多进程适合 CPU 密集型任务,不适合只是等待网络的小任务。
- 批量 CPU 任务通常使用
ProcessPoolExecutor更方便。
什么时候该考虑多进程
多进程适合 CPU 密集型任务,也就是程序主要在“算”,而不是“等”。例如:
- 批量图片处理;
- 大量数据转换;
- 压缩、加密、解析大文件;
- 模拟计算;
- 机器学习特征处理中的一部分 CPU 任务。
如果任务主要是请求接口、等待数据库、下载文件,多线程或 asyncio 通常更轻量。
进程之间不共享普通变量
下面这个例子很容易让初学者困惑:
from multiprocessing import Process
counter = 0
def worker():
global counter
counter += 1
print('子进程 counter =', counter)
p = Process(target=worker)
p.start()
p.join()
print('主进程 counter =', counter)子进程里看到的 counter 和主进程里的不是同一份普通内存。子进程修改它,不会自动影响主进程。
这也是多进程比多线程更“隔离”的地方:更安全,但通信更麻烦。
main 保护为什么重要
在 Windows 和 macOS 的某些启动方式下,子进程会重新导入当前模块。如果你不写:
if __name__ == '__main__':
...子进程导入模块时可能又创建新的子进程,导致程序行为混乱。
推荐模板:
from multiprocessing import Process
def worker(name):
print('hello', name)
if __name__ == '__main__':
p = Process(target=worker, args=('Alice',))
p.start()
p.join()只要写多进程,初学阶段就把这个模板当成固定习惯。
进程间传结果的朴素方式
如果只是想拿回每个任务的结果,直接使用 Process 会有点麻烦。可以用 Queue:
from multiprocessing import Process, Queue
def worker(x, q):
q.put(x * x)
if __name__ == '__main__':
q = Queue()
processes = []
for i in range(5):
p = Process(target=worker, args=(i, q))
p.start()
processes.append(p)
for p in processes:
p.join()
results = [q.get() for _ in processes]
print(results)如果任务很多,更推荐下一篇里的 ProcessPoolExecutor,代码会更简单。
多进程的成本
多进程不是免费加速:
- 创建进程比创建线程更重;
- 进程间传数据需要序列化;
- 大对象传来传去会很慢;
- 日志和调试更复杂;
- 子进程报错要正确收集。
所以多进程适合“每个任务计算量足够大”的情况。任务太小,进程开销可能比收益还大。
总结
多进程通过多个独立 Python 解释器实现真正并行,适合 CPU 密集型任务。它的代价是内存隔离、启动成本高、通信更复杂。实际项目中,通常优先使用进程池而不是手动管理大量进程。
- 多进程适合 CPU 密集型任务,能绕开 GIL,但创建和通信成本更高。
- 子进程不会自动修改主进程里的普通变量,进程间需要 Queue、Pipe 或池化接口传结果。
- 写 multiprocessing 时养成使用 `if __name__ == '__main__':` 的习惯。
版权所有
版权归属:Shuo Liu
