Python 并发编程:线程、进程与协程对比

FreeGuideOnline 最新 2026-06-16

Python 并发编程入门:线程、进程与协程全面对比

1. 为什么你需要关注并发编程?

想象一个简单的场景:你需要下载100张图片,如果一张张按顺序来,后面的图片只能干等前面的下载完成。又或者你的Web服务器在等待数据库查询时,CPU处于闲置状态,无法处理新的用户请求。并发编程就是为了解决这类“等待”造成的资源浪费,让程序在同样的时间内完成更多工作。

Python 提供了三种主流的并发编程方式:

  • 多线程(Threading):在同一个进程内创建多个执行流,共享内存空间。
  • 多进程(Multiprocessing):开启多个独立的Python解释器进程,每个进程拥有独立的内存空间。
  • 协程(Coroutine / asyncio):在单线程内通过协作式切换实现并发,由程序代码显式控制任务切换。

本文将用同一套对比框架,带你理解它们的原理、适用场景与代码实现,并给出清晰的选择决策图。

2. 预备知识:并发、并行与 GIL

在深入具体技术之前,我们需要厘清几个关键概念:

  • 并发(Concurrency):多个任务在同一时间段内交替执行。即使只有一个CPU核心,也能通过快速切换任务实现“同时推进”的错觉。
  • 并行(Parallelism):多个任务在同一时刻真正同时执行,必须依赖多核CPU。
  • GIL(全局解释器锁):CPython 解释器中的一把大锁,它保证同一时刻只有一个线程可以执行 Python 字节码。这意味着多线程在CPU密集型任务中无法实现真正的并行,只能实现并发;但IO密集型任务由于会释放GIL,多线程依然能带来性能提升。
graph LR
    A[并发编程模型] --> B[多线程]
    A --> C[多进程]
    A --> D[协程]
    B --> E{受GIL限制}
    C --> F{绕过GIL}
    D --> E{受GIL限制但高效}
    E --> G[适合IO密集型]
    F --> H[适合CPU密集型]
    G --> I[资源开销小]
    H --> J[资源开销大]

3. 多线程(Threading)

3.1 工作原理

使用 threading 模块创建多个线程,它们共享同一个进程的内存(全局变量、堆等)。操作系统调度线程,线程切换由内核控制,属于抢占式多任务。当线程执行IO操作(如网络请求、文件读写)时会释放GIL,允许其他线程运行。

3.2 代码示例

import threading
import time

def download(url):
    print(f"开始下载 {url}")
    time.sleep(2)   # 模拟IO操作
    print(f"完成下载 {url}")

urls = ["file1", "file2", "file3"]

threads = []
for u in urls:
    t = threading.Thread(target=download, args=(u,))
    t.start()
    threads.append(t)

for t in threads:
    t.join()   # 等待所有线程结束

print("所有下载任务完成")

执行耗时约2秒,而不是6秒。

3.3 适用场景

  • IO密集型任务:网络爬虫、Web服务、文件读写、数据库查询等。
  • 需要避免阻塞主线程,保持用户界面响应(如桌面应用)。

3.4 主要优点

  • 创建和切换开销比进程小。
  • 共享内存,线程间通信简单(直接读取全局变量或通过 queue.Queue)。
  • 代码结构对初学者比较直观。

3.5 主要缺点

  • 受GIL限制,CPU密集型任务表现糟糕。
  • 共享数据需加锁(LockRLock),容易导致死锁、数据竞争等bug。
  • 线程数量过多会导致频繁上下文切换,反而降低性能。

4. 多进程(Multiprocessing)

4.1 工作原理

multiprocessing 模块会为每个任务启动一个独立的Python解释器进程,每个进程拥有自己的GIL和内存空间。进程间不共享内存,操作系统调度进程,可实现真正的并行计算。

4.2 代码示例

import multiprocessing
import time

def compute(n):
    print(f"进程 {multiprocessing.current_process().name} 开始计算")
    total = 0
    for i in range(10**7):
        total += i
    print(f"进程 {multiprocessing.current_process().name} 结束")
    return total

if __name__ == '__main__':
    with multiprocessing.Pool(processes=4) as pool:
        results = pool.map(compute, range(4))
    print("计算结果:", results)

多核CPU上,4个计算任务可以同时执行,时间近乎线性降低。

4.3 适用场景

  • CPU密集型任务:科学计算、图像/视频处理、机器学习推理、大文件加解密等。
  • 需要绕过GIL,充分利用多核性能的场景。

4.4 主要优点

  • 真正的并行计算,充分利用多核CPU。
  • 每个进程独立崩溃不会影响主程序(鲁棒性高)。
  • 天然的内存隔离,避免很多与共享内存相关的问题。

4.5 主要缺点

  • 进程创建和销毁的开销远大于线程。
  • 内存占用大,每个子进程需要复制一份Python解释器状态。
  • 进程间通信(PipeQueue、共享内存)复杂且序列化开销大。
  • 代码稍显臃肿,且必须放在 if __name__ == '__main__': 保护下。

5. 协程(Coroutine / asyncio)

5.1 工作原理

协程是一种用户态的并发模型。它运行在单线程内,通过 async/await 语法显式声明挂起点。当一个协程遇到 await 时,它会暂时让出控制权,让事件循环(event loop)调度其他协程执行。这被称为协作式多任务。由于切换由程序自身控制,几乎无内核切换开销。

Python 3.4+ 引入 asyncio,3.5+ 支持 async/await 语法,已成为标准库。

5.2 代码示例

import asyncio

async def download(name):
    print(f"开始下载 {name}")
    await asyncio.sleep(2)   # 模拟IO操作,使用asyncio.sleep
    print(f"完成下载 {name}")

async def main():
    tasks = [download(f"file{i}") for i in range(3)]
    await asyncio.gather(*tasks)

asyncio.run(main())

同样耗时约2秒,但整个程序只用一个线程。

5.3 适用场景

  • 高并发IO密集型任务:异步Web框架(FastAPI、aiohttp)、网络爬虫、实时聊天、数据库异步驱动。
  • 需要成千上万个并发连接且希望占用极少资源的场景。

5.4 主要优点

  • 超轻量级,可轻松创建数万个协程。
  • 切换零成本,无上下文切换开销。
  • 代码结构清晰,通过 await 直观表达异步逻辑,避免回调地狱。
  • 内存占用极低。

5.5 主要缺点

  • CPU密集型任务会阻塞整个事件循环,导致所有协程停滞。
  • 需要所有网络/IO库支持异步版本(例如使用 aiohttp 替代 requests)。
  • 调试相对复杂,栈追踪不直观。
  • 概念较多(事件循环、future、task),学习曲线略陡。

6. 终极对比:何时该用哪种?

特性 多线程 (Threading) 多进程 (Multiprocessing) 协程 (asyncio)
核心模块 threading multiprocessing asyncio
任务切换者 操作系统(抢占式) 操作系统(抢占式) 程序自身(协作式)
并行能力 受GIL限制,单核并发 真正的多核并行 单线程内并发
内存占用 中等 (MB级) 高 (每个进程独立内存) 极低 (KB级)
创建/切换开销 极小
数据共享与通信 简单(共享内存) 复杂(序列化、管道、队列) 无需特殊机制(单线程)
适用任务类型 IO密集型 CPU密集型 超高并发IO密集型
典型并发数量级 几十到几百 几个到几十(受CPU核心限制) 数千至上万
代码风格 传统函数式 类似线程,需保护入口点 async/await 异步风格

决策流程图

面临多个任务需要并发执行?
  │
  ├── 任务是CPU密集型? → 使用 多进程
  │
  ├── 任务是IO密集型,且并发量不大(<100)? → 使用 多线程
  │
  └── 任务是IO密集型,且需要超高并发(1000+)或很少的内存? → 使用 协程 (asyncio)

7. 实战案例:IO密集 vs CPU密集场景对比

我们通过两个典型任务来直观感受三者的效率差异。

案例A:模拟IO密集型(网络请求)

任务:发起10次“网络请求”,每次等待0.5秒。
测试代码框架类似,记录总耗时。

大致结果(典型相对耗时):

  • 串行执行:约5秒
  • 多线程(10线程):约0.5秒
  • 协程(asyncio):约0.5秒
  • 多进程:约0.5秒,但进程创建开销更大,可能略慢

结论:IO密集型任务中,多线程和协程都能极大地提升性能,且损耗很低。多进程虽然也行,但重武器没必要。

案例B:模拟CPU密集型(计算斐波那契数)

任务:重复计算40次斐波那契(递归,可加大计算量)。
在多核CPU上,纯Python耗时如下:

  • 串行计算:假设10秒
  • 多线程:约10秒(甚至稍慢,因为GIL竞争)
  • 协程:10秒(单线程无帮助)
  • 多进程(4进程):约2.6秒

结论:CPU密集型必须用多进程,多线程和协程无能为力。

8. 避免常见坑点

  • 多线程的锁:访问共享数据必须使用 threading.Lock,否则会出现竞态条件。推荐使用 queue.Queue 做线程间任务分发。
  • 多进程的序列化:传递给子进程的参数必须可序列化(pickle)。lambda、闭包等可能失败。
  • 协程的阻塞:绝不能在协程中调用同步阻塞函数(如 time.sleep()requests.get()),必须使用异步版本的库,否则整个事件循环会被卡死。
  • 混用:可以在一个项目中组合使用多进程 + 协程(例如每个进程内运行一个事件循环),但不要轻易混合多线程和协程,除非你清楚了解 asyncio 的线程安全性。
  • 守护线程:用 setDaemon(True) 可以让线程在主线程结束时自动退出,避免程序无法终止。

9. 进阶建议与学习路径

  1. concurrent.futures 开始:它提供了高层的 ThreadPoolExecutorProcessPoolExecutor,用统一的接口管理线程池和进程池,是新手快速上手的最佳选择。
  2. 深入 asyncio:理解事件循环、Future、Task 和 await 的实质(yield from的语法糖),建议阅读官方文档和经典教程。
  3. 掌握异步生态:学会使用 aiohttp(HTTP客户端/服务器)、asyncpg(PostgreSQL异步驱动)、aioredis 等,才能真正发挥协程的威力。
  4. 性能分析:使用 cProfilepy-spy 等工具确认瓶颈究竟是CPU还是IO,再针对性选择并发模型。
  5. 考虑替代方案:如果CPU密集型任务非常繁重,Python原生的多进程可能也不够高效,可以结合C扩展(如Cython、C模块),或将任务分发到分布式任务队列(Celery)。

10. 总结

Python的并发编程世界清晰且实用:IO密集用协程,CPU密集用多进程,简单的IO任务多线程够用。切记不要拿着一把锤子看什么都是钉子。理解底层GIL机制和三大模型的本质差异,你将能设计出高效、简洁、稳健的并发程序。现在,打开编辑器,选择你手头的一个IO任务,用 asyncio 重写它,亲身体验并发的魔力吧。