Python学习之------multiprocessing 多进程编程
-
- 目录
- [1. 为什么需要多进程](#1. 为什么需要多进程)
-
- [1.1 先说 GIL](#1.1 先说 GIL)
- [1.2 什么时候用多进程](#1.2 什么时候用多进程)
- [2. 核心概念:进程、并行与并发](#2. 核心概念:进程、并行与并发)
- [3. 写多进程代码前的三个铁律](#3. 写多进程代码前的三个铁律)
-
- [铁律一:入口代码必须放在 `if name == "main":` 里](#铁律一:入口代码必须放在
if __name__ == "__main__":里) - [铁律二:传给进程的函数和参数必须能被 pickle 序列化](#铁律二:传给进程的函数和参数必须能被 pickle 序列化)
- 铁律三:进程之间不共享内存,全局变量在子进程里是"复制品"
- [铁律一:入口代码必须放在 `if name == "main":` 里](#铁律一:入口代码必须放在
- [4. 第一个多进程程序:Process 类](#4. 第一个多进程程序:Process 类)
- [5. 进程池 Pool(最常用)](#5. 进程池 Pool(最常用))
-
- [5.1 创建进程池](#5.1 创建进程池)
- [5.2 `pool.map`:同一函数、不同参数,阻塞等待整批结果](#5.2
pool.map:同一函数、不同参数,阻塞等待整批结果) - [5.3 `pool.starmap`:同一函数、多个参数](#5.3
pool.starmap:同一函数、多个参数) - [5.4 `pool.apply`:换函数也行,但是阻塞、单任务](#5.4
pool.apply:换函数也行,但是阻塞、单任务) - [5.5 `pool.apply_async`:不同函数、不同参数,真正的异步并发](#5.5
pool.apply_async:不同函数、不同参数,真正的异步并发) - [5.6 异步批量:`map_async`](#5.6 异步批量:
map_async) - [5.7 边算边取:`imap` / `imap_unordered`](#5.7 边算边取:
imap/imap_unordered) - [5.8 资源释放:close / join / terminate / with](#5.8 资源释放:close / join / terminate / with)
- [6. 阻塞到底阻塞谁?](#6. 阻塞到底阻塞谁?)
- [7. 进程间通信:Queue 与 Pipe](#7. 进程间通信:Queue 与 Pipe)
-
- [7.1 Queue:多进程共享的 FIFO 队列](#7.1 Queue:多进程共享的 FIFO 队列)
- [7.2 JoinableQueue:确认任务被处理完](#7.2 JoinableQueue:确认任务被处理完)
- [7.3 Pipe:两个进程之间的双向管道](#7.3 Pipe:两个进程之间的双向管道)
- [8. 共享状态:Value / Array / Manager](#8. 共享状态:Value / Array / Manager)
-
- [8.1 Value / Array:共享一块 ctypes 内存(快)](#8.1 Value / Array:共享一块 ctypes 内存(快))
- [8.2 Manager:共享 dict / list / 命名空间(灵活但慢)](#8.2 Manager:共享 dict / list / 命名空间(灵活但慢))
- [9. 同步原语](#9. 同步原语)
- [10. Pool 的高级用法](#10. Pool 的高级用法)
-
- [10.1 `initializer`:每个 worker 启动时初始化一次](#10.1
initializer:每个 worker 启动时初始化一次) - [10.2 `maxtasksperchild`:worker 干满 N 个任务后自动重启](#10.2
maxtasksperchild:worker 干满 N 个任务后自动重启) - [10.3 `chunksize`:任务打包粒度](#10.3
chunksize:任务打包粒度) - [10.4 `callback`:任务完成时自动回调](#10.4
callback:任务完成时自动回调)
- [10.1 `initializer`:每个 worker 启动时初始化一次](#10.1
- [11. 常见坑与调试技巧](#11. 常见坑与调试技巧)
-
- [11.1 用 lambda 或局部函数 → pickle 失败](#11.1 用 lambda 或局部函数 → pickle 失败)
- [11.2 忘了 `.get()` → 异常被静默吞掉](#11.2 忘了
.get()→ 异常被静默吞掉) - [11.3 忘了 `close()` / `join()` → 程序提前退出或卡死](#11.3 忘了
close()/join()→ 程序提前退出或卡死) - [11.4 全局变量不共享](#11.4 全局变量不共享)
- [11.5 死锁](#11.5 死锁)
- [11.6 Windows 上 spawn 的副作用](#11.6 Windows 上 spawn 的副作用)
- [11.7 print 输出交错 / 乱码](#11.7 print 输出交错 / 乱码)
- [11.8 进程数不要贪多](#11.8 进程数不要贪多)
- [11.9 调试小技巧](#11.9 调试小技巧)
- [12. 现代替代:ProcessPoolExecutor](#12. 现代替代:ProcessPoolExecutor)
- [13. 完整实战示例](#13. 完整实战示例)
-
- [示例 A:CPU 密集并行计算(对比串行/并行)](#示例 A:CPU 密集并行计算(对比串行/并行))
- [示例 B:不同类型任务并发(apply_async + callback)](#示例 B:不同类型任务并发(apply_async + callback))
- [示例 C:生产者-消费者(Queue 协作)](#示例 C:生产者-消费者(Queue 协作))
- [示例 D:共享计数器(Value + Lock)](#示例 D:共享计数器(Value + Lock))
- [14. 速查表](#14. 速查表)
- 最后
目录
- 为什么需要多进程
- 核心概念:进程、并行与并发
- 写多进程代码前的三个铁律
- [第一个多进程程序:Process 类](#第一个多进程程序:Process 类)
- [进程池 Pool(最常用)](#进程池 Pool(最常用))
- 阻塞到底阻塞谁?
- [进程间通信:Queue 与 Pipe](#进程间通信:Queue 与 Pipe)
- [共享状态:Value / Array / Manager](#共享状态:Value / Array / Manager)
- 同步原语
- [Pool 的高级用法](#Pool 的高级用法)
- 常见坑与调试技巧
- 现代替代:ProcessPoolExecutor
- 完整实战示例
- 速查表
1. 为什么需要多进程
1.1 先说 GIL
CPython(官方 Python)有一个叫 GIL(全局解释器锁) 的机制:同一时刻,一个进程内只有一个线程在执行 Python 字节码。
这带来的结果是:
- CPU 密集型任务 (大量计算、循环、数值处理):多线程几乎不加速,因为线程被 GIL 串行化了。
- IO 密集型任务 (网络请求、读写文件、等待数据库):多线程有效,因为线程在等待 IO 时会释放 GIL,让其他线程跑。
多进程的解决思路:每个进程有自己独立的解释器、独立的内存、独立的 GIL,所以多个进程可以真正同时跑在多个 CPU 核上,CPU 密集任务才能吃满多核。
1.2 什么时候用多进程
| 场景 | 推荐方案 |
|---|---|
| CPU 密集(纯计算) | 多进程(multiprocessing / ProcessPoolExecutor) |
| IO 密集(网络、文件) | 多线程 或 asyncio(多进程也可以,但开销大) |
| 既要并发又要共享复杂数据 | 多进程 + Manager / Queue |
| 海量小任务、需要统一管理 | 进程池 Pool |
一句话:想让计算真正跑满多核 → 用多进程。
2. 核心概念:进程、并行与并发
- 进程(Process) :操作系统里资源分配的基本单位,有独立的内存空间。进程之间默认不共享任何数据。
- 线程(Thread):进程内部的执行单元,共享进程内存,受 GIL 限制。
- 并发(Concurrency):多个任务"看起来"同时推进(交替执行)。
- 并行(Parallelism):多个任务"真正"同时执行(多核同时干活)。
多进程给你的是并行 。代价是:进程间通信、数据传递、创建销毁的开销都比较大(尤其 Windows 上是 spawn,创建进程要重新导入模块),所以任务太碎太小就不值得用多进程。
3. 写多进程代码前的三个铁律
铁律一:入口代码必须放在 if __name__ == "__main__": 里
python
import multiprocessing
def work(x):
return x * x
# 错误示范:直接写在模块顶层,Windows 上会无限递归创建子进程!
# pool = multiprocessing.Pool(4)
# print(pool.map(work, range(4)))
if __name__ == "__main__":
pool = multiprocessing.Pool(4)
print(pool.map(work, range(4))) # [0, 1, 4, 9]
原因:Windows 没有 fork,启动子进程时会重新导入一遍主模块,如果顶层代码直接创建进程,子进程导入时又创建进程......变成"子子孙孙无穷尽"。
铁律二:传给进程的函数和参数必须能被 pickle 序列化
进程间传参数/返回值本质是"序列化 → 传给子进程 → 反序列化"。以下东西不能直接传:
lambda匿名函数- 函数内部定义的局部函数
- 生成器对象
- 锁、socket、打开的文件句柄等
所以 worker 函数要定义成模块级别的普通函数,不要用 lambda。
铁律三:进程之间不共享内存,全局变量在子进程里是"复制品"
python
import multiprocessing
counter = 0 # 全局变量
def increment():
global counter
counter += 1
if __name__ == "__main__":
ps = [multiprocessing.Process(target=increment) for _ in range(10)]
for p in ps:
p.start()
for p in ps:
p.join()
print(counter) # 还是 0!子进程改的是各自的副本
想要跨进程共享,必须显式用 Queue、Value、Manager 等(见第 7、8 节)。
4. 第一个多进程程序:Process 类
最底层的用法:直接创建 Process 对象,指定要执行的函数和参数。
python
from multiprocessing import Process
import time
def work(name, seconds):
print(f"{name} 开始干活", flush=True)
time.sleep(seconds)
print(f"{name} 干完了", flush=True)
if __name__ == "__main__":
# 创建 3 个进程(此时还没启动)
processes = [
Process(target=work, args=(f"进程{i}", 1)) for i in range(3)
]
# start():启动;join():等待该进程结束
for p in processes:
p.start()
for p in processes:
p.join()
print("全部完成")
args是元组 ,单参数也要写(x,)。start()让进程真正跑起来;join()让主进程等它结束。flush=True用于防止 print 输出被缓冲导致顺序错乱。
Process 常用属性和方法:
| 成员 | 作用 |
|---|---|
p.start() |
启动进程 |
p.join(timeout=None) |
等待进程结束,可设超时 |
p.terminate() |
强制杀死进程(尽量少用) |
p.is_alive() |
是否还活着 |
p.pid |
进程 ID |
p.name |
进程名 |
p.daemon |
设为 True 时,主进程退出会连带杀掉它 |
Process也支持with写法(Python 3.7+):退出时自动join()。
注意 :Process 的 target 函数没有返回值通道 ,直接 return 主进程拿不到。要拿结果,用 Queue(见第 7 节)或直接上 Pool(见第 5 节)。
5. 进程池 Pool(最常用)
每次手动创建/销毁进程开销很大。进程池 预先创建一批 worker 进程,把任务塞给它们,跑完再复用。绝大多数多进程场景用 Pool 就够了。
5.1 创建进程池
python
from multiprocessing import Pool
if __name__ == "__main__":
pool = Pool(4) # 4 个 worker 进程;不写则默认 CPU 核数
...
pool.close()
pool.join()
5.2 pool.map:同一函数、不同参数,阻塞等待整批结果
python
from multiprocessing import Pool
def square(x):
return x * x
if __name__ == "__main__":
with Pool(4) as pool:
results = pool.map(square, [1, 2, 3, 4, 5])
print(results) # [1, 4, 9, 16, 25](按输入顺序)
- 第一个参数是同一个函数 ,第二个参数是可迭代的任务参数。
- 返回列表,顺序和输入一致。
- 主进程会阻塞到整批任务全部完成。
5.3 pool.starmap:同一函数、多个参数
python
def add(a, b):
return a + b
if __name__ == "__main__":
tasks = [(1, 10), (2, 20), (3, 30)]
with Pool(4) as pool:
results = pool.starmap(add, tasks)
print(results) # [11, 22, 33]
starmap 会把元组拆开传给函数,等价于多次调用 add(1, 10)、add(2, 20)......
5.4 pool.apply:换函数也行,但是阻塞、单任务
python
def calc_salary(base, bonus):
return base + bonus
def calc_tax(salary, rate):
return salary * rate
if __name__ == "__main__":
with Pool(4) as pool:
salary = pool.apply(calc_salary, args=(10000, 2000))
tax = pool.apply(calc_tax, args=(salary, 0.1))
print(salary, tax) # 12000 1200.0
apply 一次只提交一个 任务,主进程等它跑完才继续,没有并发。它存在的意义是"借用进程池里的进程",适合少量、按顺序、又要换函数的调用。
5.5 pool.apply_async:不同函数、不同参数,真正的异步并发
python
from multiprocessing import Pool
def calc_salary(base, bonus):
return base + bonus
def calc_tax(salary, rate):
return salary * rate
def calc_overtime(hours, hourly):
return hours * hourly
if __name__ == "__main__":
with Pool(4) as pool:
# 提交后立即返回,主进程不等待
results = [
pool.apply_async(calc_salary, args=(10000, 2000)),
pool.apply_async(calc_tax, args=(12000, 0.1)),
pool.apply_async(calc_overtime, args=(20, 150)),
]
# 需要结果时再逐个 get()(这里才真正等待)
salary, tax, overtime = [r.get() for r in results]
print(salary, tax, overtime) # 12000 1200.0 3000
要点:
- 每个任务可以完全自由:换函数、换参数数量、换参数类型都行。
- 提交后立刻返回
AsyncResult对象,不会阻塞主进程。 - 结果要调
.get()取;任务里抛的异常也会在.get()时重新抛出来 ,所以必须每个都get(),否则异常会被悄悄吞掉。 .get(timeout=5)可设超时,超时抛multiprocessing.TimeoutError。
AsyncResult 常用方法:
| 方法 | 作用 |
|---|---|
r.get(timeout=None) |
阻塞直到拿到结果(异常会重新抛出) |
r.wait(timeout=None) |
只等待,不拿结果 |
r.ready() |
是否已完成 |
r.successful() |
是否成功(需先 ready) |
5.6 异步批量:map_async
map 的异步版:提交后立即返回,主进程可以继续做别的事,需要结果时再 .get()。
python
if __name__ == "__main__":
with Pool(4) as pool:
result = pool.map_async(square, range(1000))
print("主进程先干别的......") # 不会阻塞
values = result.get() # 需要时再等
同理有 starmap_async(对应 starmap)。
5.7 边算边取:imap / imap_unordered
map 会把全部结果攒在内存里;任务量很大时用 懒迭代 更省内存:
python
if __name__ == "__main__":
with Pool(4) as pool:
# imap:边算边出结果,顺序和输入一致
for res in pool.imap(square, range(10)):
print(res)
# imap_unordered:谁先算完谁先出,不保证顺序(吞吐更高)
for res in pool.imap_unordered(square, range(10)):
print(res)
5.8 资源释放:close / join / terminate / with
python
if __name__ == "__main__":
pool = Pool(4)
results = [pool.apply_async(square, args=(i,)) for i in range(10)]
pool.close() # 1. 不再接收新任务(已提交的继续执行)
pool.join() # 2. 等所有任务跑完,然后回收 worker 进程
values = [r.get() for r in results]
| 方法 | 作用 |
|---|---|
pool.close() |
停止接收新任务,已提交任务继续跑 |
pool.join() |
阻塞直到所有任务完成、进程回收(必须先 close 或 terminate) |
pool.terminate() |
立即杀掉所有 worker,丢弃未完成任务 |
with Pool(n) as pool: |
退出时自动 close() + join(),推荐 |
两个经典错误:
- 只
close()不join():程序可能在任务没跑完时就退出了。 - 只
join()不close():join()会一直等新任务,直接卡死。
6. 阻塞到底阻塞谁?
这是新手最容易误解的地方。以 pool.map 为例:
串行: 任务1 → 任务2 → 任务3 → 任务4 (总时间 = 4 × 单任务耗时)
并发: 任务1 ─┐
任务2 ─┤ 4 个 worker 进程同时跑 (总时间 ≈ 单任务耗时 + 开销)
任务3 ─┤
任务4 ─┘
主进程: 提交后停住等待整批结果
- worker 进程里的
work函数完全不阻塞:每个 worker 拿到自己的任务就闷头执行,互相之间没有排队关系。 - 阻塞的是主进程 :
pool.map提交完任务后,主进程在map这一行停住,等所有 worker 的结果回来才继续往下走。
实测对比(4 个各耗时 1 秒的任务,4 进程):
python
import time
from multiprocessing import Pool
def work(n):
time.sleep(n)
return n
if __name__ == "__main__":
tasks = [1, 1, 1, 1]
t0 = time.perf_counter()
seq = [work(t) for t in tasks]
print(f"串行: {time.perf_counter() - t0:.2f}s") # 约 4.0s
t0 = time.perf_counter()
with Pool(4) as pool:
par = pool.map(work, tasks)
print(f"pool.map(4进程): {time.perf_counter() - t0:.2f}s") # 约 1.0s
加速来自任务的并行执行 ,不是来自"主进程不等待"。如果主进程在等待期间还有别的事要做,就改用 apply_async / map_async(非阻塞),或者用 imap 边算边取。
7. 进程间通信:Queue 与 Pipe
进程间内存不共享,数据传递要靠通信机制。
7.1 Queue:多进程共享的 FIFO 队列
python
from multiprocessing import Process, Queue
def producer(q):
for i in range(5):
q.put(f"数据{i}") # 放入队列
def consumer(q):
while True:
item = q.get() # 阻塞等待取数据
if item is None: # 约定 None 为结束信号
break
print("收到:", item)
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(None) # 发送结束信号
p2.join()
print("完成")
常用方法:put(item)、get()、empty()、qsize()。
注意:
empty()/qsize()在多进程环境下不可靠 (可能竞态),别依赖它们做精确判断;用"结束信号"(如None)是更稳妥的收尾方式。
7.2 JoinableQueue:确认任务被处理完
python
from multiprocessing import Process, JoinableQueue
def consumer(q):
while True:
item = q.get()
if item is None:
q.task_done()
break
print("处理:", item)
q.task_done() # 告诉队列这个任务处理完了
if __name__ == "__main__":
q = JoinableQueue()
p = Process(target=consumer, args=(q,))
p.start()
for i in range(5):
q.put(i)
q.put(None)
p.join()
print("完成")
7.3 Pipe:两个进程之间的双向管道
python
from multiprocessing import Process, Pipe
def worker(conn):
conn.send("hello") # 发数据
msg = conn.recv() # 收数据
print("worker 收到:", msg)
if __name__ == "__main__":
parent, child = Pipe() # 返回两端,默认双向
p = Process(target=worker, args=(child,))
p.start()
print("主进程收到:", parent.recv())
parent.send("hi")
p.join()
Pipe()返回两端,默认duplex=True双向;Pipe(duplex=False)则单向。- 适合两个进程之间通信;多个进程共用一个管道端时要注意加锁。
- 多个进程间广播/多对多,优先用
Queue。
8. 共享状态:Value / Array / Manager
8.1 Value / Array:共享一块 ctypes 内存(快)
python
from multiprocessing import Process, Value
def increment(shared):
with shared.get_lock(): # 自动加锁,防止并发写坏数据
shared.value += 1
if __name__ == "__main__":
v = Value("i", 0) # "i" 表示 C 的 int
ps = [Process(target=increment, args=(v,)) for _ in range(10)]
for p in ps:
p.start()
for p in ps:
p.join()
print(v.value) # 10
常用类型码:"i" int、"d" double、"f" float、"b" signed char、"c" char 等。
数组同理:
python
from multiprocessing import Array
a = Array("d", [0.0] * 5) # 5 个 double
with a.get_lock():
a[0] = 3.14
8.2 Manager:共享 dict / list / 命名空间(灵活但慢)
python
from multiprocessing import Manager, Process
def worker(d, lst):
d["key"] = "value" # 通过代理对象操作
lst.append(1)
if __name__ == "__main__":
with Manager() as mgr:
d = mgr.dict() # 共享字典
lst = mgr.list() # 共享列表
ns = mgr.Namespace() # 共享命名空间(属性访问)
ps = [Process(target=worker, args=(d, lst)) for _ in range(4)]
for p in ps:
p.start()
for p in ps:
p.join()
print(d, lst)
Manager通过"代理对象"让多个进程操作同一个数据结构,使用简单,但每次访问都有 IPC 开销,比Value慢不少。- 高频共享的简单数值用
Value,需要复杂结构(dict/list/自定义对象)用Manager,能不用共享内存就尽量不用------优先用返回值 / Queue 传递。
9. 同步原语
当多个进程要协调执行顺序 或保护共享资源 时,用这些原语(用法和 threading 基本一致):
| 原语 | 用途 |
|---|---|
Lock / RLock |
互斥锁,保护临界区(同一时刻只允许一个进程进入) |
Semaphore(n) |
信号量,最多允许 n 个进程同时通过(限流) |
Event |
事件,一个进程 set(),其他进程 wait()(通知机制) |
Barrier(n) |
栅栏,n 个进程都到了才一起放行 |
Condition |
条件变量,基于 Lock 的复杂等待/通知 |
python
from multiprocessing import Process, Lock
def safe_print(lock, msg):
with lock: # 加锁,防止多进程 print 交错
print(msg, flush=True)
if __name__ == "__main__":
lock = Lock()
ps = [Process(target=safe_print, args=(lock, f"进程{i}")) for i in range(5)]
for p in ps:
p.start()
for p in ps:
p.join()
Event 示例(通知机制):
python
from multiprocessing import Process, Event
import time
def waiter(ev):
print("等待信号......", flush=True)
ev.wait() # 阻塞直到 set()
print("收到信号,继续干活", flush=True)
if __name__ == "__main__":
ev = Event()
p = Process(target=waiter, args=(ev,))
p.start()
time.sleep(1)
ev.set() # 发出信号
p.join()
10. Pool 的高级用法
10.1 initializer:每个 worker 启动时初始化一次
适合"每个进程只连接一次数据库 / 加载一次模型"的场景,避免每个任务重复初始化:
python
from multiprocessing import Pool
def init_worker():
global conn
conn = connect_db() # 模拟:每个 worker 启动时连一次
def query(user_id):
return conn.fetch(user_id) # 直接复用进程内的 conn
if __name__ == "__main__":
with Pool(4, initializer=init_worker) as pool:
results = pool.map(query, [1, 2, 3, 4])
注意:
conn是该进程内的全局变量,主进程访问不到,也不需要访问------每个 worker 各用各的。
10.2 maxtasksperchild:worker 干满 N 个任务后自动重启
防止单个 worker 长期运行导致内存泄漏累积:
python
with Pool(4, maxtasksperchild=100) as pool:
...
10.3 chunksize:任务打包粒度
map 底层会把任务分批发给 worker(chunksize 表示一批几个)。任务又碎又多时,调大 chunksize 能减少进程间通信次数:
python
with Pool(4) as pool:
results = pool.map(square, range(100_000), chunksize=1000)
(imap 默认 chunksize=1,追求吞吐可调大。)
10.4 callback:任务完成时自动回调
apply_async / map_async 支持 callback(成功回调)和 error_callback(失败回调),回调在主进程里执行:
python
from multiprocessing import Pool
def square(x):
return x * x
def on_done(result):
print("任务完成,结果是:", result)
def on_error(exc):
print("任务出错:", exc)
if __name__ == "__main__":
with Pool(4) as pool:
pool.apply_async(square, args=(9,), callback=on_done, error_callback=on_error)
pool.close()
pool.join()
11. 常见坑与调试技巧
11.1 用 lambda 或局部函数 → pickle 失败
python
# ❌ 错误:lambda 不能被 pickle
with Pool(4) as pool:
pool.map(lambda x: x * 2, range(10))
# ✅ 正确:模块级普通函数
def double(x):
return x * 2
with Pool(4) as pool:
pool.map(double, range(10))
11.2 忘了 .get() → 异常被静默吞掉
apply_async 的任务出错不会立刻报错,只在 .get() 时抛出。每个异步结果都要 get(),否则你根本不知道任务挂了。
11.3 忘了 close() / join() → 程序提前退出或卡死
- 只 close 不 join:任务没跑完程序就退。
- 只 join 不 close:
join永远等不到"没有新任务"的确认,卡死。 - 用
with Pool(...)省心。
11.4 全局变量不共享
子进程里改全局变量,主进程看不到(见铁律三)。需要共享就用 Value / Manager / Queue。
11.5 死锁
典型场景:持有锁的时候去 join() 等待别人,而别人也在等锁 → 互相等死。
python
# ❌ 错误:持锁等 join
with lock:
p.join() # 子进程如果也要这把锁,就死锁了
排查技巧 :join(timeout=5) 设置超时,或者用 p.is_alive() 判断是否卡住。
11.6 Windows 上 spawn 的副作用
spawn 会重新导入主模块,所以:
- 顶层代码不要做重活(会被每个子进程重复执行一遍);
- 入口必须放
if __name__ == "__main__":里。
11.7 print 输出交错 / 乱码
多个进程同时 print 到终端会交错。方案:加 Lock,或用 logging 模块(线程/进程安全)。
11.8 进程数不要贪多
进程数超过 CPU 核数不仅不加速,反而因为上下文切换 变慢。经验值:multiprocessing.cpu_count() 或略少。
python
import multiprocessing
n = multiprocessing.cpu_count()
with Pool(n) as pool:
...
11.9 调试小技巧
- 先在小数据集 + 少量进程上验证逻辑,再放大。
- 怀疑死锁:给
join()/get()加timeout。 - 任务函数里
print(..., flush=True)方便观察执行过程。
12. 现代替代:ProcessPoolExecutor
标准库 concurrent.futures.ProcessPoolExecutor 是进程池的高层封装 ,API 更简洁,与线程池 ThreadPoolExecutor 用法一致。
python
from concurrent.futures import ProcessPoolExecutor
def square(x):
return x * x
if __name__ == "__main__":
with ProcessPoolExecutor(max_workers=4) as ex:
# 批量(返回迭代器,顺序与输入一致,逐个取)
results = list(ex.map(square, range(10)))
# 单个异步任务(类似 apply_async)
fut = ex.submit(square, 42)
print(fut.result()) # 1764
需要"谁先完成先处理谁"时,配合 as_completed:
python
from concurrent.futures import ProcessPoolExecutor, as_completed
if __name__ == "__main__":
with ProcessPoolExecutor(max_workers=4) as ex:
futures = [ex.submit(square, i) for i in range(10)]
for fut in as_completed(futures):
print(fut.result()) # 完成的顺序不固定
什么时候用哪个?
| 需求 | 推荐 |
|---|---|
| 日常批量计算、想要简洁 API | ProcessPoolExecutor |
需要 Queue/Pipe/Value/Manager/Lock 等底层能力 |
multiprocessing 原生 Process + Pool |
| 不同函数、不同参数混杂 | pool.apply_async |
| 边算边取、省内存 | pool.imap_unordered |
| 需要 worker 初始化 / 任务数上限重启 | Pool(initializer=..., maxtasksperchild=...) |
13. 完整实战示例
示例 A:CPU 密集并行计算(对比串行/并行)
python
import time
from multiprocessing import Pool
def is_prime(n):
"""朴素素数判断,故意写得慢一点方便对比"""
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
def count_primes(limit):
return sum(1 for x in range(limit) if is_prime(x))
if __name__ == "__main__":
tasks = [100_000, 200_000, 300_000, 400_000]
t0 = time.perf_counter()
serial = [count_primes(t) for t in tasks]
print(f"串行: {time.perf_counter() - t0:.2f}s -> {serial}")
t0 = time.perf_counter()
with Pool(4) as pool:
parallel = pool.map(count_primes, tasks)
print(f"并行: {time.perf_counter() - t0:.2f}s -> {parallel}")
示例 B:不同类型任务并发(apply_async + callback)
python
from multiprocessing import Pool
import time
def download(url):
time.sleep(1)
return f"{url} 下载完成"
def compress(data):
time.sleep(1)
return f"压缩 {data} 完成"
def notify(result):
print("回调:", result, flush=True)
if __name__ == "__main__":
with Pool(3) as pool:
pool.apply_async(download, args=("http://a.com",), callback=notify)
pool.apply_async(download, args=("http://b.com",), callback=notify)
pool.apply_async(compress, args=("大文件.zip",), callback=notify)
pool.close()
pool.join()
示例 C:生产者-消费者(Queue 协作)
python
from multiprocessing import Process, Queue
import time, random
def producer(q, n):
for i in range(n):
q.put(i)
time.sleep(random.uniform(0.1, 0.3))
q.put(None) # 结束信号
def consumer(q):
while True:
item = q.get()
if item is None:
break
print(f"消费者处理: {item * 10}", flush=True)
if __name__ == "__main__":
q = Queue()
p1 = Process(target=producer, args=(q, 10))
p2 = Process(target=consumer, args=(q,))
p1.start(); p2.start()
p1.join(); p2.join()
示例 D:共享计数器(Value + Lock)
python
from multiprocessing import Process, Value, Lock
def work(counter, lock):
for _ in range(100):
with lock:
counter.value += 1
if __name__ == "__main__":
counter = Value("i", 0)
lock = Lock()
ps = [Process(target=work, args=(counter, lock)) for _ in range(8)]
for p in ps:
p.start()
for p in ps:
p.join()
print("最终计数:", counter.value) # 800
14. 速查表
Pool 方法速查
| 方法 | 阻塞? | 能否换函数 | 返回 | 适用 |
|---|---|---|---|---|
apply(f, args=()) |
阻塞(单任务) | ✅ | 直接结果 | 少量顺序任务 |
apply_async(f, args=()) |
不阻塞 | ✅ | AsyncResult,.get() |
不同函数/参数并发 |
map(f, iterable) |
阻塞(整批) | ❌ 同函数 | 列表(保序) | 批量同函数 |
map_async(f, iterable) |
不阻塞 | ❌ | AsyncResult |
批量 + 主进程不等待 |
starmap(f, iterable) |
阻塞 | ❌ | 列表 | 同函数多参数 |
starmap_async(f, iterable) |
不阻塞 | ❌ | AsyncResult |
同函数多参数 + 异步 |
imap(f, iterable) |
懒迭代 | ❌ | 迭代器(保序) | 海量任务省内存 |
imap_unordered(f, iterable) |
懒迭代 | ❌ | 迭代器(无序) | 海量任务最大吞吐 |
释放流程速查
Pool 收尾: pool.close() → pool.join() (或直接 with Pool(...))
强制停止: pool.terminate() → pool.join()
决策速查
任务很多、同函数 → pool.map / ProcessPoolExecutor.map
不同函数、不同参数 → pool.apply_async
需要边算边取、省内存 → pool.imap_unordered
主进程还要干别的事 → apply_async / map_async
需要跨进程共享数据 → Value / Array / Manager / Queue
需要协调多个进程的顺序 → Lock / Event / Barrier / Condition
最后
写多进程代码时,请时刻记住三个铁律:
- 入口放
if __name__ == "__main__":; - 传给进程的东西必须能 pickle(用模块级函数);
- 全局变量不共享,共享要显式用
Queue/Value/Manager。
从 pool.map 起步,按需升级到 apply_async、Queue、Manager,这套知识就够覆盖 90% 的多进程需求了。