三种并发模型
| 模型 | 适合 | 不适合 |
|---|---|---|
| 多线程 threading | I/O 密集(网络 / 文件) | CPU 密集(被 GIL 锁住) |
| 多进程 multiprocessing | CPU 密集(数学 / 图像处理) | 进程开销大、共享数据麻烦 |
| 异步 asyncio | 大量 I/O 等待(网络爬虫 / 服务) | CPU 计算(会卡住事件循环) |
GIL:全局解释器锁
CPython 一次只允许一个线程执行 Python 字节码。所以:
- CPU 密集多线程没用——多个线程也只有一个在跑
- I/O 密集多线程有用——等 I/O 时 GIL 会释放给别的线程
这就是为什么 CPU 密集要用多进程。
一个例子直观感受
import time
from threading import Thread
from multiprocessing import Process
def cpu_heavy():
s = 0
for i in range(10_000_000):
s += i
return s
# 串行
t = time.time()
cpu_heavy()
cpu_heavy()
print(f"串行: {time.time()-t:.2f}s") # 0.5s
# 多线程
t = time.time()
threads = [Thread(target=cpu_heavy) for _ in range(2)]
for x in threads: x.start()
for x in threads: x.join()
print(f"多线程: {time.time()-t:.2f}s") # 0.5s(GIL 锁住,没快)
# 多进程
t = time.time()
procs = [Process(target=cpu_heavy) for _ in range(2)]
for x in procs: x.start()
for x in procs: x.join()
print(f"多进程: {time.time()-t:.2f}s") # 0.25s(真并行)
I/O 密集:多线程 vs 异步
import requests
from concurrent.futures import ThreadPoolExecutor
urls = [...] # 100 个 URL
def fetch(url):
return requests.get(url).text
# 多线程:100 个线程同时等
with ThreadPoolExecutor(max_workers=20) as pool:
results = list(pool.map(fetch, urls))
# 异步:单线程,但同时等
import asyncio
import aiohttp
async def fetch_async(session, url):
async with session.get(url) as r:
return await r.text()
async def main():
async with aiohttp.ClientSession() as session:
tasks = [fetch_async(session, url) for url in urls]
return await asyncio.gather(*tasks)
results = asyncio.run(main())
异步比多线程更轻量、能开更多并发——线程是有上限的(数千),asyncio 协程能开几十万。
concurrent.futures:高级 API
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# 线程池:I/O 密集
with ThreadPoolExecutor(max_workers=10) as pool:
results = pool.map(fetch, urls)
# 进程池:CPU 密集
with ProcessPoolExecutor(max_workers=4) as pool:
results = pool.map(cpu_heavy, data_chunks)
不需要管线程对象、Lock、Queue——用 Executor 就够。
何时选谁:决策树
任务是 CPU 还是 I/O?
├── CPU 密集 → multiprocessing / ProcessPoolExecutor
└── I/O 密集
├── 简单 / 现有库不支持 async → ThreadPoolExecutor
└── 高并发 + 框架支持(aiohttp / httpx)→ asyncio
进程 vs 线程的额外区别
| 维度 | 线程 | 进程 |
|---|---|---|
| 创建开销 | 小(毫秒) | 大(百毫秒) |
| 内存共享 | 默认共享 | 默认隔离 |
| 通信 | 直接读写共享变量 | Queue / Pipe / shared memory |
| 数据传递 | 引用 | pickle 序列化(要可序列化) |
| 崩溃影响 | 整进程死 | 单进程死,主进程没事 |
别用 threading 当万金油
新手常见错误:所有并发都上 threading。事实上:
- 简单 I/O 几个:
ThreadPoolExecutor足够 - 高并发 I/O:用
asyncio - CPU 密集:用
ProcessPoolExecutor - 极致性能:直接 C 扩展 / Rust / Go
下一篇深入讲 asyncio。