第 11 章 · 并发模型¶
并发是你作为资深 Java 工程师的强项——你熟悉线程、锁、ExecutorService、CompletableFuture。但 Python 有一个让所有 Java 程序员翻车的东西:GIL(全局解释器锁)。本章讲透 GIL,对照 threading/multiprocessing/concurrent.futures,并给 asyncio 一个概览。
11.1 GIL:为什么 Python 多线程"不能"并行 ⚠️¶
GIL(Global Interpreter Lock) 是 CPython 实现的一个机制:同一时刻,只有一个线程能执行 Python 字节码。
后果:
- CPU 密集型任务,多线程无法利用多核——甚至比单线程更慢(多了锁竞争和切换开销)。
- IO 密集型任务(网络、磁盘),多线程仍然有效——因为线程在等 IO 时会释放 GIL,其他线程能跑。
⚠️ Java 程序员的陷阱(最致命的一条)
别拿 Java 直觉写 Python 的 CPU 密集多线程,以为能加速。一段纯计算的多线程代码在 Python 里不会变快。要 CPU 并行,用 multiprocessing(见 11.3)。
为什么有 GIL:CPython 用引用计数管理内存,GIL 简化了线程安全的实现。它是实现细节,不是语言规范——其他实现(Jython、IronPython)没有 GIL;CPython 3.13 起也有实验性的 free-threading(no-GIL)构建,但 2026 年默认仍带 GIL。
| 场景 | GIL 影响 | 推荐 |
|---|---|---|
| CPU 密集(数值计算) | ❌ 多线程无益 | multiprocessing 或 C 扩展/NumPy |
| IO 密集(网络/文件) | ✅ 等待时释放 GIL | threading / asyncio |
| 混合 | 视情况 | concurrent.futures |
11.2 threading ↔ java.lang.Thread¶
-
Java
-
Python
注意:
threading.Thread接收target(函数)和args——因为函数是一等公民(第 2 章)。- 锁用
with lock:(上下文管理器),比 Java 的try/finally释放更优雅,且保证异常时释放。 - 即便有锁,CPU 密集仍受 GIL 限制——锁保护的是数据一致性,不解决并行性。
11.3 multiprocessing:绕过 GIL 实现真并行¶
要 CPU 多核并行,用多进程——每个进程有独立的解释器和 GIL,真正并行:
-
Java(多线程即可并行)
-
Python(多进程才并行)
代价:进程比线程重(独立内存空间),进程间通信(队列、管道)要序列化,开销大。所以 multiprocessing 适合"任务大、计算重、少通信"的场景。
Java 程序员的视角
在 Java 你用多线程做 CPU 并行;在 Python 你得用多进程。这是最大的思维切换。NumPy/C 扩展能在内部释放 GIL 实现真正的多线程并行,所以数值计算走 NumPy 而非手写多进程。
11.4 concurrent.futures:高层抽象 ↔ ExecutorService¶
最常用的"提交任务、拿结果"抽象,对应 Java 的 ExecutorService:
-
Java
-
Python
ThreadPoolExecutor:IO 密集(受 GIL,但等待时让出)。ProcessPoolExecutor:CPU 密集(真并行,内部用 multiprocessing)。pool.map(func, items):批量提交 + 按序收集,最省心。
选择建议
不确定就先用 concurrent.futures——它屏蔽了线程/进程的细节,API 统一,是"日常并行"的默认选择。
11.5 asyncio 概览:单线程高并发 ↔ 响应式 / 虚拟线程¶
asyncio 是 Python 的协程并发模型——单线程内用事件循环切换任务,实现极高并发(上万连接),没有线程切换开销。
import asyncio
async def fetch(url: str) -> str:
await asyncio.sleep(1) # await 让出控制权,事件循环去跑别的任务
return f"done {url}"
async def main():
# 并发抓取所有 url(总耗时约 1 秒,而非 N 秒)
results = await asyncio.gather(*(fetch(u) for u in urls))
asyncio.run(main()) # 启动事件循环
三个关键概念:
| 概念 | 含义 | Java 对应 |
|---|---|---|
async def |
定义协程(不能直接调用,要 await) |
CompletableFuture 链 |
await |
等待一个协程完成(期间让出) | .thenApply/.join |
asyncio.gather |
并发执行多个协程 | CompletableFuture.allOf |
何时用 asyncio:IO 密集 + 高并发(爬虫、Web 服务器、数据库连接池)。FastAPI/aiohttp 等现代框架都基于它。
何时不用:CPU 密集(GIL + 单线程不帮你)、简单脚本(asyncio 的心智负担不值得)。
和 Java 的对应
asyncio≈ Java 的响应式(Reactor/RxJava)或 虚拟线程(Virtual Threads, Java 21):都是为高并发 IO 设计。- 区别:asyncio 是显式 async/await 传染(一旦 async,调用链都得 async);Java 虚拟线程让你用同步代码写高并发,心智更轻。PEP 703 的 free-threading(no-GIL)已在 3.13 提供实验性构建,未来或成默认。
11.6 选择指南¶
你的任务是什么?
│
├─ CPU 密集(数值/计算)
│ └─ multiprocessing / ProcessPoolExecutor / NumPy
│
├─ IO 密集,并发量小(几个~几十)
│ └─ threading / ThreadPoolExecutor
│
└─ IO 密集,并发量大(成百上千连接)
└─ asyncio(配合 aiohttp/FastAPI)
一句话总结
Java 里"多线程"是万能解;Python 里你得先分清 CPU 还是 IO:CPU 走多进程,IO 小并发走多线程,IO 大并发走 asyncio。
本章练习¶
练习 11.1
解释为什么下面这段"多线程加速计算"在 Python 里往往比单线程更慢:
from threading import Thread
def heavy(n): s = sum(i*i for i in range(n))
threads = [Thread(target=heavy, args=(10**7,)) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
参考答案
heavy 是纯 CPU 计算,受 GIL 限制——四个线程同一时刻只有一个在真正执行 Python 字节码,无法并行。加上线程切换和锁竞争的开销,总时间反而比单线程顺序跑四次更长。要加速应改用 multiprocessing.Pool,让每个进程独立 GIL 真正并行。
练习 11.2
用 concurrent.futures.ThreadPoolExecutor 并发请求一批 URL(用 urllib.request.urlopen),收集状态码。
参考答案
from concurrent.futures import ThreadPoolExecutor
import urllib.request
def status(url):
try:
with urllib.request.urlopen(url, timeout=5) as r:
return url, r.status
except Exception as e:
return url, str(e)
urls = ["https://example.com", "https://python.org"]
with ThreadPoolExecutor(max_workers=8) as pool:
for url, code in pool.map(status, urls):
print(url, code)
练习 11.3
用 asyncio 改写:并发 sleep 多个不同时长,总耗时约等于最长那个(而非总和)。
参考答案
上一章:第 10 章 · 工程化基础 | 下一章:第 12 章 · 常见陷阱与反 Java 习惯。