3、多进程编程:multiprocessing模块、进程池、进程间通信(Queue、Pipe)
聊到多进程,我得先跟你交个底。
Python 的 GIL(全局解释器锁)是个老生常谈的话题。说白了,它让多线程在 CPU 密集型任务上成了摆设。你开十个线程,它们还是在同一颗核心里排队。那怎么办?
多进程。每个进程都有自己的 GIL,互不干扰。这才是真正的并行。
3.1 multiprocessing 模块:开箱即用的多进程
Python 标准库里的 multiprocessing,用起来跟 threading 很像。但底层完全不同。我刚开始用的时候,差点以为它就是线程的翻版。其实不是,它启动的是独立的操作系统进程。
from multiprocessing import Process
import os
def worker(name):
print(f"子进程 {name} 启动,PID: {os.getpid()}")
if __name__ == "__main__":
print(f"主进程 PID: {os.getpid()}")
p = Process(target=worker, args=("A",))
p.start()
p.join()
print("子进程结束")
嗯,这里要注意:if __name__ == "__main__" 不是摆设。在 Windows 上,没有这行代码,进程会递归创建,直接报错。我在项目里吃过这个亏,调试了半天才发现是启动入口的问题。
if __name__ 保护,结果进程像病毒一样疯狂 fork,直接把服务器搞崩了。记住,跨平台代码一定要加。
3.2 进程池:别让进程数失控
你想想看,如果任务有几百个,你开几百个进程?
操作系统会疯掉的。进程切换的开销远大于线程,而且每个进程都有独立的内存空间。所以,进程池是必须的。
from multiprocessing import Pool
import time
def cpu_intensive(n):
# 模拟 CPU 密集计算
return sum(i * i for i in range(n))
if __name__ == "__main__":
with Pool(processes=4) as pool:
results = pool.map(cpu_intensive, [10**7, 10**7, 10**7, 10**7])
print(results)
我个人习惯把进程数设为 cpu_count() 或者 cpu_count() - 1。留一个核给系统,免得机器卡死。
pool.imap() 代替 pool.map(),可以边计算边拿结果,不用等全部跑完。适合需要实时展示进度的场景。
3.3 进程间通信:Queue 和 Pipe
进程之间不共享内存。这是多进程和多线程最大的区别。你想让两个进程交换数据,得用专门的通信机制。
3.3.1 Queue:生产者-消费者模式
multiprocessing.Queue 是线程安全的,也是进程安全的。它底层用了管道和锁,你只管往里放、往外取就行。
from multiprocessing import Process, Queue
def producer(q):
for i in range(5):
q.put(f"消息 {i}")
print(f"生产: 消息 {i}")
def consumer(q):
while True:
msg = q.get()
if msg == "STOP":
break
print(f"消费: {msg}")
if __name__ == "__main__":
q = Queue()
p1 = Process(target=producer, args=(q,))
p2 = Process(target=consumer, args=(q,))
p1.start()
p2.start()
p1.join()
q.put("STOP") # 发送停止信号
p2.join()
我在项目中遇到过一个问题:Queue 里的数据太多,消费者处理不过来,内存就爆了。后来我加了 q.maxsize 限制,生产者满了就阻塞,完美解决。
3.3.2 Pipe:双向通信,但要注意死锁
Pipe 比 Queue 轻量。它返回两个连接对象,一个发一个收。但 Pipe 不是线程安全的,多个进程同时读写可能会出问题。
from multiprocessing import Process, Pipe
def worker(conn):
conn.send("你好,主进程")
msg = conn.recv()
print(f"子进程收到: {msg}")
conn.close()
if __name__ == "__main__":
parent_conn, child_conn = Pipe()
p = Process(target=worker, args=(child_conn,))
p.start()
print(f"主进程收到: {parent_conn.recv()}")
parent_conn.send("你好,子进程")
p.join()
3.4 进程间同步:Lock 和 Event
虽然进程不共享内存,但如果你操作共享文件、数据库或者打印机,还是需要同步的。
from multiprocessing import Process, Lock
def print_with_lock(lock, num):
with lock:
print(f"进程 {num} 正在打印")
if __name__ == "__main__":
lock = Lock()
processes = [Process(target=print_with_lock, args=(lock, i)) for i in range(5)]
for p in processes:
p.start()
for p in processes:
p.join()
说白了,Lock 就是一把全局锁。谁拿到谁干活。Event 更灵活,可以用来做进程间的信号通知,比如「数据准备好了,开始处理」。
3.5 实战建议:什么时候用多进程?
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| CPU 密集型(图像处理、科学计算) | 多进程 + Pool | 绕过 GIL,真正并行 |
| I/O 密集型(网络请求、文件读写) | 多线程 / asyncio | 进程开销太大,不划算 |
| 需要隔离内存(安全、稳定性) | 多进程 | 一个进程挂了不影响其他 |
| 大量小任务 | 进程池 | 避免频繁创建销毁进程 |
嗯,这一章就到这里。下一章我们聊聊异步编程的另一个利器——协程。你会发现,有时候连进程都不需要,一个事件循环就能搞定一切。