1. 为什么需要多进程?
在Python中处理CPU密集型任务时,你可能会发现即使使用了多线程,程序性能也没有明显提升。这是因为Python的全局解释器锁(GIL)的存在,它使得同一时刻只有一个线程能够执行Python字节码。我曾在处理图像批量处理任务时,发现使用多线程反而比单线程更慢——这正是GIL的典型表现。
多进程(multiprocessing)是Python中绕过GIL限制的有效方案。每个进程都有自己独立的Python解释器和内存空间,因此可以真正实现并行计算。当我在处理一个需要同时运行多个机器学习模型预测的项目时,多进程将任务完成时间从4小时缩短到了40分钟。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多进程基础实现
2.1 Process类的基本用法
Python的multiprocessing模块提供了Process类,使用方式与threading模块非常相似。下面是一个我经常用来演示的简单例子:
python复制from multiprocessing import Process
import os
def worker(num):
print(f'Worker {num} running in process {os.getpid()}')
if __name__ == '__main__':
processes = []
for i in range(5):
p = Process(target=worker, args=(i,))
processes.append(p)
p.start()
for p in processes:
p.join()
这里有几个关键点需要注意:
- 必须把代码放在
if __name__ == '__main__':块中,这是Windows平台下多进程编程的特殊要求 start()方法启动进程,join()等待进程结束- 每个进程都有自己独立的PID(进程ID)
2.2 进程间通信
进程之间不像线程那样共享内存,因此需要特殊的机制进行通信。我在一个分布式爬虫项目中就遇到了这个问题,最终采用了Queue方案:
python复制from multiprocessing import Process, Queue
def producer(q):
for i in range(5):
q.put(i)
print(f'Produced {i}')
def consumer(q):
while True:
item = q.get()
if item is None: # 哨兵值,表示结束
break
print(f'Consumed {item}')
if __name__ == '__main__':
q = Queue()
procs = [
Process(target=producer, args=(q,)),
Process(target=consumer, args=(q,))
]
for p in procs:
p.start()
procs[0].join() # 等待生产者结束
q.put(None) # 发送结束信号
procs[1].join()
提示:multiprocessing.Queue是线程和进程安全的,但要注意队列中的数据必须是可pickle的。
3. 进程池的高级用法
3.1 Pool的基本应用
当需要处理大量相似任务时,使用进程池(Pool)比手动管理多个Process更高效。我在处理批量文件转换时,这样使用Pool:
python复制from multiprocessing import Pool
import time
def process_file(filename):
# 模拟耗时操作
time.sleep(1)
return f"{filename} processed"
if __name__ == '__main__':
files = [f"file{i}.txt" for i in range(10)]
with Pool(4) as pool: # 4个worker进程
results = pool.map(process_file, files)
print(results)
Pool会自动管理进程的创建和回收,map方法会阻塞直到所有任务完成。在我的测试中,处理10个文件(每个耗时1秒):
- 单进程:约10秒
- 4进程池:约3秒(因为有进程创建开销)
3.2 异步处理与回调
对于需要实时处理结果的场景,可以使用apply_async:
python复制def callback(result):
print(f"Got result: {result}")
if __name__ == '__main__':
with Pool(4) as pool:
for i in range(10):
pool.apply_async(
process_file,
args=(f"file{i}.txt",),
callback=callback
)
pool.close()
pool.join() # 等待所有任务完成
这种模式特别适合Web爬虫等I/O密集型任务,我在一个需要实时保存下载内容的项目中就采用了这种方案。
4. 共享状态与同步
4.1 共享内存
虽然进程间内存隔离,但multiprocessing提供了Value和Array来实现共享数据:
python复制from multiprocessing import Process, Value, Array
def worker(n, a):
n.value += 1
for i in range(len(a)):
a[i] *= 2
if __name__ == '__main__':
num = Value('i', 0) # 'i'表示整数
arr = Array('d', [1.0, 2.0, 3.0]) # 'd'表示双精度浮点数
p = Process(target=worker, args=(num, arr))
p.start()
p.join()
print(num.value) # 输出: 1
print(arr[:]) # 输出: [2.0, 4.0, 6.0]
注意:共享变量会有性能开销,且需要特别注意同步问题。
4.2 进程锁
当多个进程需要修改共享资源时,必须使用锁来避免竞争条件。我在一个多进程日志系统中就遇到了这个问题:
python复制from multiprocessing import Process, Lock
import time
def printer(item, lock):
with lock:
print(f"Process {item} acquired the lock")
time.sleep(1) # 模拟耗时操作
if __name__ == '__main__':
lock = Lock()
items = ['A', 'B', 'C']
for item in items:
Process(target=printer, args=(item, lock)).start()
没有锁的情况下,输出可能会交错;使用锁后,每个进程的输出都是完整的。
5. 实际项目中的经验与陷阱
5.1 进程创建开销
进程创建是有成本的,特别是在Windows上。我曾经在一个需要频繁创建短期进程的项目中遇到了性能问题。解决方案是:
- 使用进程池复用进程
- 将小任务批量处理
- 考虑使用更轻量级的协程(如asyncio)处理I/O密集型任务
5.2 僵尸进程处理
如果父进程没有正确等待子进程结束,可能会导致僵尸进程。最佳实践是:
- 总是调用join()或使用上下文管理器
- 设置进程的daemon属性为True(但这样进程不能创建子进程)
- 使用signal模块处理SIGCHLD信号
5.3 跨平台兼容性问题
在Windows上,multiprocessing的实现与Unix-like系统有所不同:
- Windows使用spawn而不是fork创建进程
- 必须将代码放在
if __name__ == '__main__':块中 - 某些IPC机制在Windows上不可用
5.4 调试技巧
调试多进程程序很困难,我常用的方法是:
- 使用logging模块而不是print
- 为每个进程设置不同的日志文件
- 使用
multiprocessing.log_to_stderr()获取内部日志 - 在关键点插入调试打印
6. 性能优化策略
6.1 进程数量选择
进程数不是越多越好,最佳数量通常是:
- CPU密集型:CPU核心数
- I/O密集型:可以适当多于核心数
可以通过multiprocessing.cpu_count()获取CPU核心数:
python复制import multiprocessing
print(multiprocessing.cpu_count())
6.2 数据分块处理
对于大数据处理,应该将数据分块分配给不同进程。我在处理CSV文件时采用这样的模式:
python复制def process_chunk(chunk):
# 处理数据块
return results
if __name__ == '__main__':
data = [...] # 大数据集
chunk_size = len(data) // multiprocessing.cpu_count()
with Pool() as pool:
results = pool.map(process_chunk,
[data[i:i+chunk_size]
for i in range(0, len(data), chunk_size)])
final_result = combine(results)
6.3 内存管理
多进程会显著增加内存使用,特别是在处理大数据时:
- 考虑使用共享内存
- 使用迭代器而不是列表加载数据
- 及时释放不再需要的内存
7. 替代方案与选择
虽然multiprocessing是Python标准库中的方案,但在某些场景下,其他库可能更适合:
- concurrent.futures:更高级的接口,支持线程和进程
- joblib:特别适合科学计算任务
- dask:处理超大规模数据
- ray:分布式计算框架
我在一个需要分布式计算的项目中就选择了ray,因为它提供了更强大的功能集。但对于大多数单机并行任务,multiprocessing仍然是简单可靠的选择。
