Python multiprocessing 使用指南:突破 GIL 束缚的并行计算利器
作者:书到用时方恨少!
发布日期:2026年4月1日
阅读时长:约30分钟
📌 前言
Python 因其简洁易用而广受欢迎,但一个长期被诟病的缺点就是全局解释器锁(GIL) 。GIL 使得 Python 多线程无法真正利用多核 CPU 的优势。那么,如何让 Python 程序并行执行,充分发挥多核处理器的性能呢?答案就是 multiprocessing 模块。
multiprocessing 是 Python 标准库中的并行计算神器。它通过创建独立的子进程(而非线程)来绕过 GIL,让程序可以真正同时运行多个任务。同时,它提供了与 threading 模块几乎一致的 API,大大降低了学习成本。
无论你是需要加速数据处理、开发并行爬虫,还是构建高并发的系统,这篇博客都将带你从零开始,全面掌握 multiprocessing 的核心功能与最佳实践。
1. 📦 为什么需要 multiprocessing?
🔒 GIL 的局限
Python 的 GIL 是一个互斥锁,它保证同一时刻只有一个线程执行 Python 字节码。这意味着:
- CPU 密集型任务(如数值计算、图像处理)在多线程下无法获得性能提升,甚至因为线程切换而变慢。
- IO 密集型任务(如网络请求、文件读写)多线程仍然有效,因为线程在 IO 等待时会释放 GIL。
为了真正并行利用多核 CPU,我们需要使用多进程:每个进程拥有独立的 Python 解释器和内存空间,互不干扰,可以真正并行执行。
🚀 multiprocessing 的优势
- 绕过 GIL:进程独立运行,无锁竞争。
- 充分利用多核:可以并行执行 CPU 密集型任务。
- API 友好 :与
threading模块设计相似,易于迁移。 - 进程间通信 :提供
Queue、Pipe等多种方式。 - 进程池:方便地管理大量进程。
2. 🧱 核心概念与基本用法
2.1 Process 类
multiprocessing.Process 用于创建和管理子进程,用法与 threading.Thread 高度相似。
python
import multiprocessing
import time
def worker(name):
print(f"进程 {name} 开始")
time.sleep(2)
print(f"进程 {name} 结束")
if __name__ == "__main__":
p1 = multiprocessing.Process(target=worker, args=("A",))
p2 = multiprocessing.Process(target=worker, args=("B",))
p1.start()
p2.start()
p1.join()
p2.join()
print("所有进程结束")
关键点:
target:进程要执行的函数。args:传递给函数的参数(元组)。start():启动进程。join([timeout]):等待进程结束(阻塞)。- 必须放在
if __name__ == "__main__":中,否则在 Windows 上会引发无限递归创建进程。
2.2 守护进程与终止
daemon:设置为守护进程,主进程结束时自动终止子进程。terminate():强制终止进程(谨慎使用,可能导致资源泄漏)。
python
p = multiprocessing.Process(target=worker, args=("D",))
p.daemon = True
p.start()
# 主进程结束后,守护进程自动结束
3. 🤝 进程间通信(IPC)
进程之间不共享内存,需要通过特定机制交换数据。
3.1 Queue
multiprocessing.Queue 提供线程/进程安全的队列,用于在进程间传递消息。
python
import multiprocessing
def producer(queue):
for i in range(5):
queue.put(i)
print(f"生产: {i}")
def consumer(queue):
while True:
item = queue.get()
if item is None: # 结束信号
break
print(f"消费: {item}")
if __name__ == "__main__":
q = multiprocessing.Queue()
p1 = multiprocessing.Process(target=producer, args=(q,))
p2 = multiprocessing.Process(target=consumer, args=(q,))
p1.start()
p2.start()
p1.join()
q.put(None) # 发送结束信号
p2.join()
put(obj[, block[, timeout]]):放入对象(默认阻塞)。get([block[, timeout]]):取出对象。- 队列可以存放任意可 pickle 的对象(注意:某些对象如打开的文件句柄不能跨进程传输)。
3.2 Pipe
Pipe() 返回一对连接对象 (conn1, conn2),代表管道的两端,用于双向通信。
python
import multiprocessing
def sender(conn):
conn.send("Hello from sender")
conn.close()
def receiver(conn):
msg = conn.recv()
print(f"收到: {msg}")
if __name__ == "__main__":
parent_conn, child_conn = multiprocessing.Pipe()
p1 = multiprocessing.Process(target=sender, args=(child_conn,))
p2 = multiprocessing.Process(target=receiver, args=(parent_conn,))
p1.start()
p2.start()
p1.join()
p2.join()
send(obj):发送对象。recv():接收对象(阻塞)。- 两端可以同时发送和接收,但多个进程同时读写同一端需要自己加锁。
选择建议:
Queue更适合一对多或多对一的生产者-消费者模式。Pipe更适合两个进程之间的简单双向通信,性能更高。
4. 🔒 同步机制
当多个进程同时访问共享资源时,需要同步原语防止数据竞争。
4.1 Lock
multiprocessing.Lock 与 threading.Lock 用法一致,用于保护临界区。
python
import multiprocessing
def printer(lock, data):
with lock:
print(data)
if __name__ == "__main__":
lock = multiprocessing.Lock()
processes = []
for i in range(10):
p = multiprocessing.Process(target=printer, args=(lock, f"消息 {i}"))
processes.append(p)
p.start()
for p in processes:
p.join()
4.2 Event、Semaphore 等
Event:用于进程间信号通知。Semaphore:限制同时访问资源的进程数。Condition:更复杂的条件同步。
用法与 threading 中的同名类几乎相同,不再赘述。
5. 📦 共享内存
5.1 Value 与 Array
multiprocessing.Value 和 multiprocessing.Array 在共享内存中存储一个值或数组,可以被多个进程访问。
python
import multiprocessing
def increment(val):
with val.get_lock():
val.value += 1
if __name__ == "__main__":
shared_val = multiprocessing.Value('i', 0) # 有符号整型
processes = []
for _ in range(10):
p = multiprocessing.Process(target=increment, args=(shared_val,))
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"最终值: {shared_val.value}") # 应该是 10
- 第一个参数是类型码(如
'i'表示 int,'d'表示 double),与array模块相同。 get_lock()返回一个锁,用于安全修改。Array用于创建共享数组:multiprocessing.Array('i', [1,2,3])。
5.2 Manager
multiprocessing.Manager 提供更高级的共享对象,如列表、字典、Namespace 等,可以在不同进程间共享。它通过一个独立的服务器进程来管理对象,因此速度比 Value/Array 慢,但灵活性更高。
python
import multiprocessing
def worker(shared_dict, key, value):
shared_dict[key] = value
if __name__ == "__main__":
manager = multiprocessing.Manager()
shared_dict = manager.dict()
processes = []
for i in range(10):
p = multiprocessing.Process(target=worker, args=(shared_dict, i, i*2))
processes.append(p)
p.start()
for p in processes:
p.join()
print(shared_dict.items())
Manager 支持的类型:
dict(),list(),Namespace()Queue(),Value(),Array()等
注意 :Manager 对象在 Windows 上创建时可能较慢,且所有操作都经过序列化,性能不如直接使用 Queue 或 Pipe。
6. 🏭 进程池(Pool)
创建大量进程会消耗系统资源,进程池可以复用进程,提高效率。
6.1 基本用法
python
import multiprocessing
import time
def square(x):
time.sleep(0.1) # 模拟耗时
return x * x
if __name__ == "__main__":
with multiprocessing.Pool(processes=4) as pool:
results = pool.map(square, range(10))
print(results) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
map(func, iterable):将 iterable 中的元素分发给进程池执行,返回结果列表(保持顺序)。map_async:异步版本,返回一个AsyncResult对象。apply(func, args):同步执行单个函数。apply_async:异步执行单个函数。close():停止接受新任务。join():等待所有任务完成(必须在close后调用)。
6.2 异步与回调
python
def square(x):
return x * x
def callback(result):
print(f"结果: {result}")
if __name__ == "__main__":
with multiprocessing.Pool(4) as pool:
for i in range(10):
pool.apply_async(square, (i,), callback=callback)
pool.close()
pool.join()
6.3 使用多个参数
pool.map 只支持一个可迭代参数,如果需要多个参数,可以借助 starmap:
python
def add(a, b):
return a + b
with multiprocessing.Pool(4) as pool:
results = pool.starmap(add, [(1,2), (3,4), (5,6)])
print(results) # [3, 7, 11]
7. 💡 实战案例
案例一:并行计算素数(CPU 密集型)
python
import multiprocessing
import math
def is_prime(n):
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
limit = int(math.sqrt(n)) + 1
for i in range(3, limit, 2):
if n % i == 0:
return False
return True
def find_primes_in_range(start, end):
primes = []
for n in range(start, end):
if is_prime(n):
primes.append(n)
return primes
if __name__ == "__main__":
total = 1000000
chunk_size = total // 4
ranges = [(i*chunk_size, (i+1)*chunk_size) for i in range(4)]
ranges[-1] = (ranges[-1][0], total) # 确保覆盖到边界
with multiprocessing.Pool(4) as pool:
results = pool.starmap(find_primes_in_range, ranges)
primes = [p for sublist in results for p in sublist]
print(f"找到 {len(primes)} 个素数")
案例二:多进程爬虫(IO 密集型)
虽然多进程对 IO 密集型任务不如多线程高效,但依然可以用于提升并发度(例如避免 GIL 影响)。
python
import multiprocessing
import requests
urls = [
"https://www.example.com",
"https://www.python.org",
"https://www.github.com",
# ... 更多 URL
]
def fetch(url):
try:
response = requests.get(url, timeout=5)
return url, response.status_code, len(response.content)
except Exception as e:
return url, None, str(e)
if __name__ == "__main__":
with multiprocessing.Pool(processes=4) as pool:
results = pool.map(fetch, urls)
for url, status, data in results:
print(f"{url}: {status} - {data}")
案例三:生产者-消费者模式
使用 Queue 实现任务分发和结果收集。
python
import multiprocessing
import time
import random
def producer(queue, task_count):
for i in range(task_count):
queue.put(i)
print(f"生产任务 {i}")
time.sleep(random.random())
queue.put(None) # 结束信号
def consumer(queue, results):
while True:
task = queue.get()
if task is None:
break
result = task * task
results.append(result)
print(f"消费任务 {task} -> {result}")
if __name__ == "__main__":
q = multiprocessing.Queue()
manager = multiprocessing.Manager()
results = manager.list() # 共享列表
p1 = multiprocessing.Process(target=producer, args=(q, 10))
p2 = multiprocessing.Process(target=consumer, args=(q, results))
p1.start()
p2.start()
p1.join()
q.put(None) # 确保 consumer 退出
p2.join()
print(f"所有结果: {list(results)}")
8. ⚙️ 注意事项与性能优化
8.1 进程启动方式
不同操作系统有不同启动方式:
- spawn(Windows 默认,macOS 3.8+ 默认,Linux 可选):子进程从头开始,不继承父进程内存,安全性高,但启动较慢。
- fork(Linux 默认,macOS 旧版本):子进程继承父进程内存,启动快,但存在死锁风险(例如在多线程环境中 fork 可能复制锁状态)。
- forkserver(Linux 可选):通过一个服务器进程来 fork,避免死锁。
可以通过 multiprocessing.set_start_method() 设置启动方式。
python
import multiprocessing
multiprocessing.set_start_method('spawn') # 推荐跨平台
8.2 避免全局状态
每个进程有独立的全局变量,因此不能通过全局变量共享数据。必须使用 IPC 或共享内存。
8.3 序列化(pickle)开销
进程间传递对象需要序列化,传递大对象会显著影响性能。尽量只传递必要的数据,或使用共享内存(Value/Array)减少拷贝。
8.4 进程数设置
通常进程数设置为 CPU 核心数(multiprocessing.cpu_count()),但具体取决于任务类型:
- CPU 密集型:通常设为核心数。
- IO 密集型:可以设置更多,但受限于系统资源。
python
num_workers = multiprocessing.cpu_count()
8.5 资源释放与守护进程
确保所有进程在程序退出前正确结束,避免僵尸进程。使用 join() 等待,或设置守护进程自动终止。
8.6 异常处理
子进程中的异常不会自动传递到主进程,需要在函数内部捕获并传递状态(例如通过队列返回错误信息)。
8.7 使用 with 语句管理进程池
Pool 支持上下文管理器,确保资源正确释放。
python
with multiprocessing.Pool(4) as pool:
results = pool.map(func, data)
9. 📊 multiprocessing vs threading vs asyncio
| 特性 | multiprocessing | threading | asyncio |
|---|---|---|---|
| 适用场景 | CPU 密集型 | IO 密集型 | 高并发 IO(网络) |
| 并行性 | 真正并行(多核) | 并发(单核) | 单线程并发 |
| 内存开销 | 高(每个进程独立内存) | 低(共享内存) | 极低 |
| 进程/线程创建成本 | 高 | 中 | 极低 |
| 通信方式 | 复杂(队列、管道等) | 简单(共享变量需锁) | 简单(协程间直接调用) |
| 适用操作系统 | 跨平台 | 跨平台 | 跨平台 |
选型建议:
- CPU 密集型:
multiprocessing。 - IO 密集型(大量网络/文件):
threading或asyncio,后者更高效。 - 需要共享大量数据且频繁访问:
threading(但要小心 GIL 影响)。
10. 🎯 总结
通过本文,我们全面学习了 Python multiprocessing 模块:
- ✅ 为什么需要多进程:绕过 GIL,真正并行利用多核。
- ✅ 基本用法 :
Process的创建、启动、等待。 - ✅ 进程间通信 :
Queue、Pipe、共享内存(Value/Array/Manager)。 - ✅ 同步机制:锁、事件、信号量等。
- ✅ 进程池:高效管理大量任务。
- ✅ 实战案例:素数计算、爬虫、生产者-消费者。
- ✅ 注意事项:启动方式、序列化开销、进程数选择。
multiprocessing 是 Python 并行编程的基石,掌握它将使你在处理大规模计算和并发任务时如虎添翼。但也要注意,多进程并非万能钥匙,合理选择并发模型才是关键。
如果你在实际项目中遇到了并行计算的有趣场景,欢迎在评论区分享!感谢阅读,我们下篇见!🚀