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 上,没有这行代码,进程会递归创建,直接报错。我在项目里吃过这个亏,调试了半天才发现是启动入口的问题。

避坑指南: 我曾经在 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()
注意: 我曾经用 Pipe 做双向通信,结果两个进程同时 recv,互相等待,死锁了。后来改成半双工模式,一个进程只发,另一个只收,才稳定下来。

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 进程开销太大,不划算
需要隔离内存(安全、稳定性) 多进程 一个进程挂了不影响其他
大量小任务 进程池 避免频繁创建销毁进程
核心总结: 多进程不是银弹。它的优势在 CPU 密集和隔离性上。I/O 密集用多线程或异步就够了。别为了炫技而用多进程,性能反而会下降。

嗯,这一章就到这里。下一章我们聊聊异步编程的另一个利器——协程。你会发现,有时候连进程都不需要,一个事件循环就能搞定一切。