Python 并发编程:多线程与多进程的性能对比
Python 提供了多种并发编程方式,其中最常用的是多线程(threading)和多进程(multiprocessing)。它们在不同场景下的性能表现差异显著。本文通过实际代码测试,手把手教你如何选择适合的并发模型。
1. 理解 Python 的 GIL
了解 全局解释器锁(GIL)是理解性能差异的关键。GIL 是 CPython 解释器的一个机制,它确保同一时刻只有一个线程执行 Python 字节码。这意味着:
- CPU 密集型任务:多线程无法真正并行,因为 GIL 会串行化线程执行。
- I/O 密集型任务:多线程仍然有效,因为线程在等待 I/O 时会释放 GIL。
因此,对于计算密集型工作,应优先考虑多进程;对于网络请求、文件读写等 I/O 操作,多线程通常更轻量高效。
2. 编写测试脚本
创建 两个测试函数,分别模拟 CPU 密集型和 I/O 密集型任务。
import time
import threading
import multiprocessing
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def cpu_bound_task(n):
"""模拟 CPU 密集型任务:计算大数平方和"""
total = 0
for i in range(n):
total += i * i
return total
def io_bound_task(delay):
"""模拟 I/O 密集型任务:休眠指定秒数"""
time.sleep(delay)
return "done"
3. 测试多线程与多进程的执行时间
运行 以下代码,分别用单线程、多线程、多进程执行两类任务,并记录耗时。
def run_single(func, args_list):
start = time.time()
results = [func(arg) for arg in args_list]
end = time.time()
return end - start, results
def run_threaded(func, args_list, max_workers=4):
start = time.time()
with ThreadPoolExecutor(max_workers=max_workers) as executor:
results = list(executor.map(func, args_list))
end = time.time()
return end - start, results
def run_multiprocess(func, args_list, max_workers=4):
start = time.time()
with ProcessPoolExecutor(max_workers=max_workers) as executor:
results = list(executor.map(func, args_list))
end = time.time()
return end - start, results
执行 性能测试:
if __name__ == "__main__":
# CPU 密集型测试:10 次大循环
cpu_args = [5_000_000] * 10
print("=== CPU 密集型任务 ===")
t1, _ = run_single(cpu_bound_task, cpu_args)
t2, _ = run_threaded(cpu_bound_task, cpu_args)
t3, _ = run_multiprocess(cpu_bound_task, cpu_args)
print(f"单线程: {t1:.2f} 秒")
print(f"多线程: {t2:.2f} 秒")
print(f"多进程: {t3:.2f} 秒")
# I/O 密集型测试:10 次 1 秒休眠
io_args = [1.0] * 10
print("\n=== I/O 密集型任务 ===")
t1, _ = run_single(io_bound_task, io_args)
t2, _ = run_threaded(io_bound_task, io_args)
t3, _ = run_multiprocess(io_bound_task, io_args)
print(f"单线程: {t1:.2f} 秒")
print(f"多线程: {t2:.2f} 秒")
print(f"多进程: {t3:.2f} 秒")
4. 分析典型测试结果
在一台 4 核 CPU 的普通笔记本上运行上述代码,典型输出如下:
=== CPU 密集型任务 ===
单线程: 8.45 秒
多线程: 8.60 秒
多进程: 2.30 秒
=== I/O 密集型任务 ===
单线程: 10.02 秒
多线程: 1.05 秒
多进程: 1.20 秒
将结果整理为表格:
| 任务类型 | 执行方式 | 耗时(秒) | 相对效率 |
|---|---|---|---|
| CPU 密集型 | 单线程 | 8.45 | 基准 |
| CPU 密集型 | 多线程 | 8.60 | ≈1x |
| CPU 密集型 | 多进程 | 2.30 | ≈3.7x |
| I/O 密集型 | 单线程 | 10.02 | 基准 |
| I/O 密集型 | 多线程 | 1.05 | ≈9.5x |
| I/O 密集型 | 多进程 | 1.20 | ≈8.3x |
观察结论:
- CPU 密集型任务:多进程显著优于多线程,因为绕过了 GIL 限制,真正利用多核。
- I/O 密集型任务:多线程与多进程性能接近,但多线程启动更快、内存开销更小。
5. 如何选择并发模型
根据任务类型做决策:
-
如果是 CPU 密集型任务(如数值计算、图像处理、加密解密):
- 使用
multiprocessing或concurrent.futures.ProcessPoolExecutor。 - 避免 使用
threading,它几乎不会带来加速。
- 使用
-
如果是 I/O 密集型任务(如 HTTP 请求、数据库查询、文件读写):
- 优先使用
threading或concurrent.futures.ThreadPoolExecutor。 - 原因:线程创建开销小,切换成本低,且 I/O 阻塞时会自动释放 GIL。
- 优先使用
-
如果任务混合了 CPU 和 I/O:
- 拆分任务:将 CPU 部分交给进程池,I/O 部分交给线程池。
- 或使用 异步编程(
asyncio),但这属于另一套并发模型。
6. 注意事项与陷阱
警惕 以下常见问题:
- 进程间通信开销大:
multiprocessing中传递大量数据会显著拖慢速度。尽量让每个进程独立完成完整子任务,减少共享数据。 - 线程不适用于计算加速:不要被“多线程”名字误导,在 CPython 中它不能加速纯 Python 计算。
- 资源限制:创建过多进程可能导致系统内存不足或上下文切换开销过大。通常进程数设为 CPU 核心数即可。
- 可移植性:
multiprocessing在 Windows 和 macOS 上使用spawn启动方式,需将入口代码放在if __name__ == '__main__':下,否则可能无限递归创建进程。
验证 你的环境核心数:
import os
print(f"CPU 核心数: {os.cpu_count()}")
建议将 max_workers 参数设为该值或略高(如 +1),以获得最佳性能。
7. 扩展:何时考虑 asyncio?
考虑 使用 asyncio 当你有大量 I/O 操作且希望更低的内存占用。例如:
- 同时发起数百个 HTTP 请求。
- 处理高并发 WebSocket 连接。
但注意:asyncio 是单线程事件循环,不能用于 CPU 密集型任务,除非配合 loop.run_in_executor() 将计算任务提交给线程池或进程池。
import asyncio
async def fetch(url):
# 模拟异步 I/O
await asyncio.sleep(1)
return f"Response from {url}"
async def main():
urls = ["http://a.com"] * 10
tasks = [fetch(url) for url in urls]
results = await asyncio.gather(*tasks)
return results
# 运行
asyncio.run(main())
这种方式在 I/O 密集场景下比多线程更节省资源,但学习曲线较陡。
总结选择策略:
- 纯计算? → 多进程
- 纯 I/O? → 多线程 或 asyncio
- 不确定? → 先写单线程版本,再用
time模块实测对比

暂无评论,快来抢沙发吧!