进程 vs 线程与异步初探
本教程共 70 篇 · 第 57 篇 · 更新于 2026-07-22 · 约 5 分钟阅读
57. 进程 vs 线程与异步初探
本节目标:搞清楚什么时候用进程、什么时候用线程,理解 GIL 的影响,学会用
concurrent.futures简化并发代码。
前面两节分别讲了多进程和多线程,但你可能会问:到底选哪个?这节就来掰扯清楚,并引出现代 Python 里更省心的写法。
GIL:Python 线程的隐形天花板
GIL 全称 Global Interpreter Lock,是 CPython 解释器里的一把全局锁。它的存在保证了同一时刻只有一个线程在执行 Python 字节码,避免了解释器内部数据结构的竞争问题。
这把锁带来的直接后果是:纯 Python 的多线程无法利用多核 CPU。你开 8 个线程跑计算密集型任务,实际上只占满 1 个核心,另外 7 个核心在旁边看戏。
import threading, time
def cpu_bound():
count = 0
for _ in range(50_000_000):
count += 1
start = time.time()
t1 = threading.Thread(target=cpu_bound)
t2 = threading.Thread(target=cpu_bound)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"多线程耗时: {time.time() - start:.2f} 秒")
start = time.time()
cpu_bound()
cpu_bound()
print(f"单线程顺序耗时: {time.time() - start:.2f} 秒")
在 CPython 上跑这段代码,你会发现两种方式的耗时差不多,甚至多线程还略慢——因为线程切换本身也有开销。
NoteGIL 只影响 CPython。如果你用 PyPy、Jython 或者其他实现,行为可能不同。另外,线程在执行 C 语言扩展(如 NumPy 的矩阵运算)时,会释放 GIL,这时多线程可以真正并行。
计算密集型 vs IO 密集型
选进程还是选线程,核心看你的任务类型。
计算密集型:大量数学运算、图像处理、视频编码。CPU 一直在干活,很少等待。这类任务用多进程最合适,每个进程有独立的 GIL,可以跑满所有核心。
IO 密集型:网络请求、文件读写、数据库查询。大部分时间花在等网络上,CPU 利用率很低。这类任务用多线程或异步 IO 更合适,因为切换代价小,能在等待时去做别的事。
打个比方:计算密集型像厨师一直在切菜,线程切换等于让他放下刀、换围裙、再拿起刀,反而耽误事;IO 密集型像厨师把汤炖上后,空闲时间去炒菜, multitasking 就合理了。
concurrent.futures:更现代的接口
Python 3.2 引入了 concurrent.futures 模块,把进程池和线程池包装成统一的接口。你不再需要手动管理 Process 或 Thread,代码更清爽。
线程池版:
from concurrent.futures import ThreadPoolExecutor
import time
def fetch(url):
time.sleep(1)
return f"{url} 完成"
urls = ['url-1', 'url-2', 'url-3', 'url-4']
with ThreadPoolExecutor(max_workers=4) as executor:
results = executor.map(fetch, urls)
for r in results:
print(r)
进程池版只需把 ThreadPoolExecutor 换成 ProcessPoolExecutor,其他代码几乎不变:
from concurrent.futures import ProcessPoolExecutor
def cpu_task(n):
return sum(range(n))
numbers = [10_000_000, 20_000_000, 30_000_000]
with ProcessPoolExecutor(max_workers=3) as executor:
results = executor.map(cpu_task, numbers)
print(list(results))
Tip
as_completed()适合「谁先做完先处理谁」的场景,不必等全部任务结束:from concurrent.futures import ThreadPoolExecutor, as_completed with ThreadPoolExecutor() as ex: futures = {ex.submit(fetch, u): u for u in urls} for future in as_completed(futures): print(futures[future], future.result())
异步 IO 初探
多线程虽然能并发 IO,但线程数量一多,操作系统调度开销也会上涨。有没有更轻量的方案?
现代操作系统支持异步 IO:程序发起一个网络请求后不必阻塞等待,操作系统会在数据就绪后通知程序。Python 3.4+ 的 asyncio 模块就是基于这个机制,用单线程+事件循环实现高并发。一个线程可以管理成千上万个并发连接,比线程池还省资源。
这里先给你看个雏形,下两节会深入讲:
import asyncio
async def say_hello():
print("hello")
await asyncio.sleep(1)
print("world")
asyncio.run(say_hello())
async def 定义协程,await 挂起当前任务让出控制权。整个程序只有一个线程在跑,但绝不会因为等网络而傻站着。
小结
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 计算密集型 | 多进程 / ProcessPoolExecutor | 绕过 GIL,利用多核 |
| IO 密集型、中等并发 | 多线程 / ThreadPoolExecutor | 写法简单,切换开销可接受 |
| IO 密集型、高并发 | asyncio + async/await | 单线程管理大量连接,资源占用最低 |
- GIL 让 CPython 的多线程无法并行执行 Python 代码,但 IO 等待期间会释放 GIL。
concurrent.futures提供了进程池和线程池的统一封装,比底层 API 更好用。- 异步 IO 是处理高并发网络请求的趋势,后续章节会详细展开。
来源:参考了 liaoxuefeng「14.4. 进程 vs. 线程」、runoob「Python3 多线程」等,改写后所得。