目录
深入浅出理解多进程与多线程,玩转并行计算的核心
一、核心概念
(一) 进程(Process)
定义:操作系统分配资源的基本单位,拥有独立的内存空间和系统资源。
关键特性:
- 每个进程有独立的地址空间
- 进程间互不干扰(一个进程崩溃不会影响其他进程)
- 进程创建开销大(需要分配内存、创建进程控制块等)
- 进程间通信(IPC)需要专门机制(管道、消息队列、共享内存等)
(二) 线程(Thread)
定义:进程内的执行单元,是CPU调度的基本单位,共享进程的内存空间。
关键特性:
- 线程共享进程的内存空间(包括代码段、数据段、堆等)
- 线程间通信简单(直接通过共享变量)
- 线程创建开销小(只需创建线程栈和上下文)
- 线程间需要同步机制(锁、信号量等)避免竞争条件
二、关键区别对比
| 特性 | 多进程 | 多线程 |
|---|---|---|
| 内存空间 | 独立的地址空间 | 共享进程的内存空间 |
| 创建开销 | 高(需分配内存、复制数据) | 低(只需创建线程栈) |
| 通信方式 | IPC(管道、消息队列、共享内存) | 直接共享变量(需同步) |
| 同步机制 | 进程间同步(信号量、互斥锁) | 线程间同步(锁、条件变量) |
| CPU利用率 | 可充分利用多核CPU(真正并行) | 受GIL限制(Python中CPU密集型无法并行) |
| 稳定性 | 一个进程崩溃不影响其他进程 | 一个线程崩溃可能导致整个进程崩溃 |
| 适用场景 | CPU密集型任务 | I/O密集型任务(Python中) |
三、Python中的GIL(全局解释器锁)
(一)GIL是什么?
- 定义:Python解释器中的一个互斥锁(Mutex),确保同一时刻只有一个线程执行Python字节码。
- 设计目的:简化CPython的内存管理,避免多线程同时修改对象。
(二)GIL的影响
| 任务类型 | GIL影响 | 实际效果 |
|---|---|---|
| CPU密集型 | 严重限制 | 无法真正并行,多线程性能接近单线程 |
| I/O密集型 | 有限影响 | I/O等待时释放GIL,多线程可有效利用等待时间 |
示意图:
[线程A] → 执行计算 → GIL被持有 → 无法执行其他线程
[线程B] → 等待I/O → 释放GIL → 线程A可继续执行
(三)GIL工作原理
Python执行流程:
1. 获取GIL
2. 执行Python字节码(100ms)
3. 释放GIL
4. 重新获取GIL(等待其他线程释放)
I/O密集型任务:
- 线程A:执行计算 → 持有GIL
- 线程B:等待I/O → 释放GIL → 线程A继续执行
CPU密集型任务:
- 线程A:执行计算 → 持有GIL(100ms)
- 线程B:等待GIL → 无法执行
- 线程A:计算完成 → 释放GIL
- 线程B:获取GIL → 执行计算
GIL工作流程
- 线程A:执行计算 → 持有GIL(此时GIL被线程A持有)
- 线程B:开始I/O操作(如网络请求)→ 释放GIL(因为I/O等待会阻塞线程,所以线程B主动释放GIL)
- 线程A:继续执行(因为GIL仍然在它手中,它没有释放)
举例说明
- 你正在看书(线程A执行)
- 你的朋友想和你说话(线程B想执行)
- 你朋友说"我等你看完这页"(线程B释放GIL,等待)
- 你继续看完这页(线程A继续执行)
只有当你看完这页(线程A完成计算)并放下书(释放GIL)后,你的朋友才能开始说话(线程B获取GIL并执行)。
在I/O密集型任务中:
- 线程B在等待I/O时释放GIL
- 线程A继续执行,因为它已经持有GIL,不需要等待
- 线程B在I/O完成后需要等待GIL被释放才能继续执行
所以,"线程A继续执行"不是因为线程B释放GIL后"允许"线程A执行,而是因为线程A本来就在执行中,GIL没有被收回。
结论:在Python中,多线程只有在I/O等待时才有优势,CPU计算时无法并行。多进程是CPU密集型任务的唯一高效方案。
四、多进程与多线程核心特性对比
(一)核心特性对比表格
| 维度 | 多进程(Multiprocessing) | 多线程(Multithreading) |
|---|---|---|
| 工作原理 | 1. 操作系统创建新进程,分配独立内存空间 2. 通过 fork()复制父进程数据3. 进程在独立CPU核心并行运行 4. 依赖IPC机制(管道、队列)收集结果 | 1. 在现有进程内创建线程,共享进程内存空间 2. 线程竞争GIL(全局解释器锁),I/O等待时释放GIL 3. 通过锁等同步机制协调线程操作 4. 无需额外通信机制,直接共享变量 |
| 优势 | 真正并行:充分利用多核CPU资源 稳定性高:进程间内存隔离,单个进程崩溃不影响其他 无GIL限制:适配CPU密集型任务 | 低开销:线程创建、切换速度快,资源占用少 通信简单:直接共享进程内存中的变量 I/O优化:I/O等待时释放GIL,可高效利用等待时间 |
| 劣势 | 高内存开销:每个进程有独立内存,数据复制消耗资源 通信复杂:进程间需通过IPC机制交互,开发成本高 启动慢:进程创建及数据复制耗时较长 | GIL限制:CPU密集型任务无法实现多核并行,效率受限 同步复杂:需处理竞争条件、死锁等问题,易引发 bugs 稳定性低:单个线程崩溃可能导致整个进程终止 |
(二)多进程Python实现(multiprocessing)
import multiprocessing
import time
def worker(num):
"""CPU密集型任务"""
print(f"Worker {num} starting")
time.sleep(2) # 模拟CPU计算
return num * num
if __name__ == '__main__':
# 创建进程池
with multiprocessing.Pool(processes=4) as pool:
# 并行执行
results = pool.map(worker, range(10))
print("Results:", results)
(三)多线程Python实现(threading)
import threading
import time
import requests
def worker(url):
"""I/O密集型任务"""
print(f"Fetching {url}")
response = requests.get(url, timeout=5) # I/O等待
return response.status_code
if __name__ == '__main__':
urls = ["https://example.com"] * 10
threads = []
# 创建线程
for url in urls:
t = threading.Thread(target=worker, args=(url,))
threads.append(t)
t.start()
# 等待所有线程完成
for t in threads:
t.join()
六、CPU密集型 vs I/O密集型任务
| CPU密集型任务(多进程优先) | I/O密集型任务(多线程优先) | |
|---|---|---|
| 核心特征 | 1. 高CPU使用率(通常>80%) 2. 计算逻辑密集,I/O操作极少 3. 任务耗时主要由CPU计算速度决定 | 1. 低CPU使用率(通常<30%) 2. 大量I/O等待(网络、磁盘、数据库) 3. 任务耗时主要由I/O响应速度决定 |
| 典型场景 | 1. 机器学习/深度学习模型训练 2. 图像/视频编解码、特效处理 3. 大规模矩阵运算、微分方程求解 4. 物理/化学等科学模拟 | 1. 网络爬虫(批量请求网页) 2. 高频数据库查询/写入 3. 大文件批量读写 4. Web服务器并发请求处理 |
| 优先并行模型及原因 | 优先多进程 1. Python的GIL锁会阻断多线程的CPU并行执行; 2. 多进程可利用多核CPU实现真正并行; 3. 进程间隔离,不受GIL限制。 | 优先多线程 1. I/O等待时线程会释放GIL,其他线程可抢占执行; 2. 线程创建/切换开销远低于进程,适合高并发; 3. 可同时发起多个I/O请求,最大化利用等待时间。 |
| Joblib配置示例 | results = Parallel(n_jobs=-1, backend='loky')( delayed(cpu_bound_func)(arg) for arg in args)(n_jobs=-1用全部核心,loky为多进程后端) | results = Parallel(n_jobs=32, backend='threading')(delayed(io_bound_func)(arg) for arg in args)(n_jobs=32+高并发,threading为多线程后端) |
Joblib配置:
# CPU密集型任务
results = Parallel(n_jobs=-1, backend='loky')( # 默认后端
delayed(cpu_bound_function)(arg) for arg in args
)
# I/O密集型任务
results = Parallel(n_jobs=32, backend='threading')( # 显式指定多线程
delayed(io_bound_function)(arg) for arg in args
)
七、性能对比测试
(一)CPU密集型任务测试
import time
from joblib import Parallel, delayed
def cpu_task(x):
return sum(i * i for i in range(x))
# 测试多进程
start = time.time()
results = Parallel(n_jobs=-1, backend='loky')(delayed(cpu_task)(10**7) for _ in range(10))
print(f"Multi-processing time: {time.time() - start:.2f}s")
# 测试多线程(实际会慢)
start = time.time()
results = Parallel(n_jobs=4, backend='threading')(delayed(cpu_task)(10**7) for _ in range(10))
print(f"Multi-threading time: {time.time() - start:.2f}s")
预期结果:
Multi-processing time: 2.15s
Multi-threading time: 5.87s # 比多进程慢
(二) I/O密集型任务测试
import time
import requests
from joblib import Parallel, delayed
def io_task(url):
return requests.get(url, timeout=5).status_code
urls = ["https://httpbin.org/get"] * 10
# 测试多线程
start = time.time()
results = Parallel(n_jobs=16, backend='threading')(delayed(io_task)(url) for url in urls)
print(f"Multi-threading time: {time.time() - start:.2f}s")
# 测试多进程
start = time.time()
results = Parallel(n_jobs=4, backend='loky')(delayed(io_task)(url) for url in urls)
print(f"Multi-processing time: {time.time() - start:.2f}s")
预期结果:
Multi-threading time: 1.23s
Multi-processing time: 2.87s # 比多线程慢
八、最佳实践指南
(一)选择策略决策树
任务类型?
├─ CPU密集型?
│ ├─ 是 → 使用多进程(backend='loky')
│ └─ 否
├─ I/O密集型?
│ ├─ 是 → 使用多线程(backend='threading')
│ └─ 否 → 串行执行(n_jobs=1)
(二)参数配置建议
| 任务类型 | n_jobs设置 | backend | 说明 |
|---|---|---|---|
| CPU密集型 | n_jobs=-1(所有CPU核心) | 'loky'(默认) | 最大化利用CPU |
| I/O密集型 | n_jobs=2×CPU核心数 | 'threading' | 充分利用I/O等待 |
| 混合型 | 分阶段处理 | loky(CPU部分)threading(I/O部分) | 例如:先并行获取数据(I/O),再并行处理(CPU) |
(三)避免常见陷阱
- CPU密集型任务使用多线程:
# 错误示例:CPU密集型任务用多线程 results = Parallel(n_jobs=4, backend='threading')(delayed(cpu_task)(x) for x in range(10)) # 结果:比单线程慢 - I/O密集型任务使用多进程:
# 错误示例:I/O密集型任务用多进程 results = Parallel(n_jobs=4, backend='loky')(delayed(io_task)(url) for url in urls) # 结果:比多线程慢 - 不考虑任务粒度:
- 任务太小(<100ms):并行开销 > 计算时间
- 解决方案:增加任务粒度,或使用
batch_size参数
九、高级技巧
(一)混合型任务处理
from joblib import Parallel, delayed
def fetch_data(url):
"""I/O密集型部分"""
return requests.get(url).json()
def process_data(data):
"""CPU密集型部分"""
# 复杂计算
return sum(data['values'])
# 分阶段处理
urls = ["https://api.example.com/data1", "https://api.example.com/data2"]
# 1. 并行获取数据(I/O密集型,用多线程)
raw_data = Parallel(n_jobs=16, backend='threading')(
delayed(fetch_data)(url) for url in urls
)
# 2. 并行处理数据(CPU密集型,用多进程)
results = Parallel(n_jobs=-1, backend='loky')(
delayed(process_data)(data) for data in raw_data
)
(二)内存优化技巧
问题:多进程导致内存占用过高
解决方案:
# 使用内存缓存(避免重复计算)
from joblib import Memory
memory = Memory(location='./cache', verbose=0)
@memory.cache
def cpu_task(x):
return sum(i * i for i in range(x))
# 使用缓存结果
results = Parallel(n_jobs=-1, backend='loky')(
delayed(cpu_task)(x) for x in range(10)
)
(三)进度条可视化
from tqdm.auto import tqdm
from joblib import Parallel
class ProgressParallel(Parallel):
def __init__(self, use_tqdm=True, total=None, *args, **kwargs):
self._use_tqdm = use_tqdm
self._total = total
super().__init__(*args, **kwargs)
def __call__(self, *args, **kwargs):
with tqdm(disable=not self._use_tqdm, total=self._total) as self._pbar:
return super(ProgressParallel, self).__call__(*args, **kwargs)
def print_progress(self):
self._pbar.n = self.n_completed_tasks
self._pbar.refresh()
# 使用示例
results = ProgressParallel(n_jobs=4, total=100)(
delayed(cpu_task)(i) for i in range(100)
)
十、总结:核心要点
(一)选择决策
| 任务类型 | 推荐方案 | 原因 |
|---|---|---|
| CPU密集型 | 多进程 (loky) | 避免GIL,充分利用多核 |
| I/O密集型 | 多线程 (threading) | I/O等待时释放GIL,高效利用等待时间 |
| 混合型 | 分阶段处理 | I/O用多线程,CPU用多进程 |
(二)关键参数
n_jobs:- CPU密集型:
n_jobs=-1(所有核心) - I/O密集型:
n_jobs=2×CPU核心数(如8核CPU用16)
- CPU密集型:
backend:- CPU密集型:
'loky'(默认,多进程) - I/O密集型:
'threading'(显式指定)
- CPU密集型:
(三)性能优化原则
- 任务粒度:确保每个任务计算时间 > 100ms
- 资源匹配:CPU密集型匹配CPU核心数,I/O密集型匹配I/O带宽
- 避免过量并行:
n_jobs过大反而增加调度开销 - 分阶段处理:混合型任务分离I/O和CPU阶段
重要提醒:在Python中,永远不要用多线程处理CPU密集型任务!这是最常见的性能陷阱。

被折叠的 条评论
为什么被折叠?



