Python多线程threading与多进程multiprocessing怎么选
深入对比Python中threading多线程与multiprocessing多进程的核心机制、性能差异与选型依据,涵盖GIL原理、内存开销、通信机制及具体代码示例。
在 Python 并发编程中,选 threading 还是 multiprocessing,核心取决于任务类型是 I/O 密集型 还是 CPU 密集型。由于 CPython 存在全局解释器锁(GIL),多线程无法在多核 CPU 上实现 Python 字节码的真正并行运算,因此 CPU 密集型任务必须选用 multiprocessing 绕过锁限制;而网络请求、文件存取等 I/O 密集型任务,线程在等待时会主动释放 GIL,选用开销轻量、内存天然共享的 threading 则是更优解。
一、核心机制与架构差异:GIL 和内存模型
理解两个模块差异的根源,在于 Python 解释器的内部运行机制与操作系统对线程、进程的资源分配方式。
在默认的 CPython 实现中,GIL(Global Interpreter Lock,全局解释器锁)是一个互斥锁,用来防止多个原生线程同时执行 Python 字节码。即使计算机拥有 16 个核心,一个 Python 进程内的多个线程在任意具体时刻也只能有某一个线程在 CPU 上运行。因此,单纯依靠 threading 无法加速纯数值运算。
与此相对,multiprocessing 模块直接创建独立的操作系统子进程。每个子进程都有独立的 Python 解释器实例、独立的堆栈空间以及独立的 GIL。多个子进程可以被操作系统调度到不同的 CPU 核心上同步运行,实现硬件级别的真正多核并行。
二、运行时序表现:I/O 密集型与 CPU 密集型的真实耗时
很多开发者会产生疑问:既然 GIL 限制了线程并行,为什么爬虫或网络服务使用 threading 还能大幅度提速?
关键在于任务执行过程中的主动让出机制:
- I/O 密集型任务:程序的大部分时间都在等待操作系统完成网络收发、文件读取或数据库查询。当线程发起系统调用阻塞时,Python 解释器会主动释放当前拥有的 GIL。此时其他就绪线程可以立刻获取 GIL 并执行。因此,多线程能高效隐藏 I/O 延迟。
- CPU 密集型任务:代码持续进行矩阵计算、图像编解码或大量循环。线程不会因 I/O 主动让出 GIL,解释器只能按固定的执行步数强制剥夺 GIL 并触发上下文切换。由于所有线程在竞争同一个锁,多线程非但不能加速,还会因为频繁的线程切换和 CPU 缓存失效导致总耗时比单线程更慢。
三、实战选型步骤:如何做出正确决策
在实际工程开发中,选择并发模型可以通过清晰的决策步骤进行推导。不要直接凭感觉写多线程,请按以下 3 个步骤依次判断:
四、代码实现示例与现代写法
在 Python 3 中,标准库提供了统一的高级封装 concurrent.futures。它把线程池和进程池抽象出完全一致的 API 接口,极大降低了代码重构成本。
1. I/O 密集型场景(选用 ThreadPoolExecutor)
以批量下载网页或请求远程接口为例,使用线程池能够并发发出多个请求,大幅减少总等待时间:
import time
from urllib.request import urlopen
from concurrent.futures import ThreadPoolExecutor
urls = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
]
def fetch_url(url):
start = time.time()
with urlopen(url, timeout=5) as response:
code = response.getcode()
cost = time.time() - start
return f"{url} -> 状态码: {code}, 耗时: {cost:.2f}秒"
if __name__ == "__main__":
total_start = time.time()
# 创建包含 4 个工作线程的线程池
with ThreadPoolExecutor(max_workers=4) as executor:
results = executor.map(fetch_url, urls)
for res in results:
print(res)
print(f"全部请求完成,总耗时: {time.time() - total_start:.2f}秒")
每个请求延迟 1 秒,单线程依次执行需要 4 秒以上;而在 ThreadPoolExecutor 下,4 个请求并发发起,总耗时仅需约 1.1 秒,性能提升近 4 倍。
2. CPU 密集型场景(选用 ProcessPoolExecutor)
对于大范围素数筛选、复杂加密散列或图像渲染等计算任务,如果使用多线程,由于 GIL 竞争总时间基本不减甚至增加;而使用多进程能够直接将计算分配给所有核心:
import os
import time
from concurrent.futures import ProcessPoolExecutor
def heavy_computation(num):
# 模拟密集的 CPU 整数因子分解计算
count = 0
for i in range(1, num):
if num % i == 0:
count += 1
return f"子进程 PID: {os.getpid()}, 计算基数: {num}, 因子数: {count}"
if __name__ == "__main__":
# Windows 和 macOS 平台必须在 if __name__ == '__main__': 保护下启动多进程
numbers = [25_000_000, 25_000_001, 25_000_002, 25_000_003]
total_start = time.time()
# 进程数通常设置为 CPU 物理核心数或逻辑核心数
with ProcessPoolExecutor() as executor:
for result in executor.map(heavy_computation, numbers):
print(result)
print(f"多核计算完成,总耗时: {time.time() - total_start:.2f}秒")
五、开发中的常见陷阱与注意事项
虽然选型思路清晰,但在落地写代码时,多线程与多进程各自有一批容易踩坑的边界条件:
- 共享变量与线程安全(threading):多线程共享同一个进程的内存空间,读写全局变量时如果缺少互斥锁(
threading.Lock),会导致数据竞争(Race Condition)。即使是简单的count += 1,在字节码层面也包含读取、自增、写入三步,并不是原子操作。 - 必须保留
if __name__ == '__main__':(multiprocessing):在 Windows 和 macOS(Python 3.8+ 默认使用 spawn 启动模式)下,新建子进程会重新导入入口模块。若没有此代码块保护,子进程启动时会无限递归创建自身,导致内存耗尽系统崩溃。 - 参数与返回值必须支持序列化(Pickle):
multiprocessing在进程之间传递数据时,底层依赖pickle进行序列化和反序列化。如果向子进程传递匿名函数(lambda)、打开的文件句柄、数据库连接对象或未绑定的复杂生成器,会直接抛出PicklingError。 - 内存开销倍增(multiprocessing):每个子进程都需要加载自己的 Python 解释器和依赖包副本。如果主进程原本占用了 2GB 内存,启动 8 个子进程在无共享内存机制下可能会快速消耗数十 GB 物理内存。
六、全面对比与总结
为了让选择标准更加直观,下表和总结图汇总了 threading 与 multiprocessing 在六个关键维度的差异表现:
| 对比维度 | 多线程 (threading) | 多进程 (multiprocessing) |
|---|---|---|
| 内存模型 | 共享同一进程的全部内存,变量直接可见 | 独立地址空间,内存相互隔离互不干扰 |
| GIL 的影响 | 受 GIL 强约束,同一时刻只有一个核跑 Python 字节码 | 各进程拥有独立 GIL,真正实现多核并行计算 |
| 创建与切换开销 | 轻量级,微秒级上下文切换,消耗系统资源少 | 重量级,创建进程需要复制/加载上下文,开销较大 |
| 通信复杂度 | 直接访问内存,配合 queue.Queue 或锁保证安全 |
依赖 IPC 机制(multiprocessing.Queue, Pipe),需序列化 |
| 数据安全风险 | 高(易产生数据竞争,需谨慎加锁避免死锁) | 低(天然内存隔离,单个子进程崩溃通常不影响其他进程) |
| 黄金适用场景 | 网络爬虫、API 调用、GUI 后台任务、文件批量读写 | 科学计算、数据预处理、机器学习特征提取、音视频转码 |
总结来说,判断使用 threading 还是 multiprocessing 的准绳只有一个:你的瓶颈卡在网络与磁盘等待,还是卡在 CPU 的运算能力上?明确了这一点,配合现代标准的 concurrent.futures 线程池或进程池,就能在 Python 项目中兼顾开发效率与执行性能。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









