并发问题最容易被一句“Python 有 GIL”讲坏:有人因此认为线程毫无用处,也有人误以为 GIL 会自动保证数据安全。Python 3.14 又让情况多了一层——自由线程构建已经获得官方支持,但仍不是默认构建。
本文以 CPython 3.14 为基线,先按任务性质选择工具,再解释线程、进程、asyncio、取消和 GIL 的准确边界。
1. 并发、并行和异步不是同义词
- 并发(Concurrency):多个任务的生命周期重叠,可以交替推进;
- 并行(Parallelism):多个任务在同一时刻由不同计算资源执行;
- 异步(Asynchrony):调用方发起操作后不必阻塞等待,以事件或可等待对象继续协调。
选择方案先看瓶颈:
| 任务 | 常见首选 | 原因 |
|---|---|---|
| 阻塞式文件、网络、设备 I/O | threading / ThreadPoolExecutor | 能复用同步库,等待期间可推进其他线程 |
| 高并发异步网络 I/O | asyncio | 单线程事件循环可管理大量协作式任务 |
| 纯 Python CPU 密集计算 | multiprocessing / ProcessPoolExecutor | 默认 CPython 中绕开进程内 GIL,利用多核 |
| 可隔离、可序列化的 CPU 任务 | InterpreterPoolExecutor | 每个工作线程使用独立解释器和独立 GIL,可在单进程内利用多核 |
| 支持自由线程的 CPU 工作负载 | 自由线程 CPython + threading,经实测决定 | 可并行执行 Python 代码,但生态与同步设计需验证 |
这只是起点。数据复制成本、第三方库是否释放 GIL、部署平台和故障隔离也会改变结论,最终要用真实工作负载测量。
2. 默认 CPython 的 GIL 到底保证什么
全局解释器锁(Global Interpreter Lock,GIL)在默认 CPython 构建中,通常只允许一个线程在同一时刻执行 Python 字节码并操作 Python 对象。线程在阻塞 I/O 时会释放 GIL,部分 C 扩展也会在长时间本地计算时主动释放它。
GIL 不等于:
- 整个 Python 语句都是原子的;
- 多步业务操作天然线程安全;
- 容器复合操作不会竞态;
- 内存可见性和事务一致性已经得到业务层保证。
下面的“先检查再写入”由多个步骤组成:
| |
两个线程可能都发现键不存在,然后重复加载并覆盖。正确方案取决于语义:可以用锁保护复合不变量,也可以使用允许重复计算但结果幂等的设计。
| |
这个版本在锁内执行加载,能避免重复加载,却也让慢 I/O 阻塞其他键。生产代码可能使用每键锁、Future 占位或锁外加载后再合并,必须围绕业务不变量权衡。
3. Python 3.14 的自由线程构建
CPython 3.13 开始提供可禁用 GIL 的自由线程构建;Python 3.14 根据 PEP 779 进入“官方支持但可选”的阶段。它并没有取代默认构建,也不意味着所有扩展和程序自动获得线性加速。
可以检查当前运行时:
| |
sys._is_gil_enabled() 在 Python 3.14 可报告当前进程是否启用了 GIL。自由线程构建仍可在运行时启用 GIL;导入未声明兼容自由线程的 C 扩展时,也可能自动重新启用 GIL 并给出警告。
自由线程不取消同步需求,恰恰会让真实并行下的数据竞争更容易暴露。迁移前要检查:
- 第三方二进制扩展是否提供兼容构建;
- 共享可变状态是否有明确所有权或锁;
- 代码是否依赖“看起来原子”的 CPython 偶然行为;
- 单线程性能、内存开销和扩展兼容性是否可接受;
- 基准是否覆盖真实请求、数据规模和硬件。
4. 线程:适合阻塞 I/O 和同步库
高层接口 ThreadPoolExecutor 比手工管理线程更适合一批独立任务:
| |
注意边界:
future.result()会重新抛出工作线程异常;- 超时要传给实际 I/O API,不能只给等待 Future 的调用加超时;
- 线程池大小不是越大越好,受远端限流、连接数、内存和调度影响;
- 关闭执行器默认会等待任务结束,进程不会靠“忘记引用”可靠退出。
共享数据使用 Lock、RLock、Condition、Semaphore、Event 或线程安全队列协调。优先通过队列传递数据、缩小共享状态,而不是给每个字段随手加锁。
5. 进程:隔离地址空间并利用多核
ProcessPoolExecutor 把可序列化任务提交给子进程:
| |
入口保护是跨平台进程代码的必要习惯。提交的函数、参数和返回值通常需要可 pickle;REPL 中定义的局部函数、lambda、打开的连接和锁通常不适合直接传递。
Python 3.14 之后,fork 不再是任何平台的默认启动方式。支持通过 Unix 管道传递文件描述符的 POSIX 平台(如 Linux)默认使用 forkserver,macOS 和 Windows 默认使用 spawn。不要假设所有系统都使用同一种方式,库也不应擅自全局设置启动方式。确需指定时,由应用入口通过上下文显式选择,并验证第三方库兼容性。
进程有序列化、启动和数据复制成本。很小的任务可能比单进程更慢;把工作分成足够大的批次,并避免在进程间来回搬运巨量对象。
6. Python 3.14 的子解释器执行器
Python 3.14 新增 InterpreterPoolExecutor。它是 ThreadPoolExecutor 的子类,但每个工作线程运行在独立解释器中;每个解释器拥有独立 GIL,因此默认构建也能让纯 Python 任务在不同 CPU 核上并行:
| |
隔离是它的核心边界:不同解释器拥有各自的模块、sys、内置对象和运行时状态,不能直接共享普通可变对象。执行器会使用 pickle 传递初始化器、任务参数和返回值,因此函数必须可导入、数据必须可序列化,传输成本也不会消失。
与进程池相比,子解释器仍处于同一操作系统进程,故障隔离和扩展兼容性不同;与自由线程相比,它通过隔离减少共享状态,而不是让多个线程共同操作同一套对象。它是 Python 3.14 值得评估的新选择,不是对进程池或自由线程的全面替代。
7. asyncio:协作式并发
协程遇到 await 才把控制权交回事件循环:
| |
TaskGroup 提供结构化并发:离开上下文前等待所有任务;某个任务以普通异常失败时,会取消其余任务,并在清理后以异常组传播失败。它比创建一批无人持有、无人等待的后台任务更容易管理生命周期。
async 函数调用只创建协程对象,不会自动执行:
| |
需要 await coroutine、创建 Task,或由 asyncio.run() 驱动顶层协程。
8. 不要阻塞事件循环
下面的同步休眠会阻塞整个事件循环线程:
| |
异步等待应使用:
| |
无法替换的短期阻塞 I/O 可以交给线程:
| |
to_thread() 主要用于不会长时间占用 GIL 的阻塞调用。默认 GIL 构建下,把纯 Python CPU 密集函数扔给线程通常不会获得多核并行。
9. 超时、取消和清理
| |
这里的 load_bytes() 代表项目提供的异步 I/O 函数;超时必须包住真正的等待操作。
超时通过取消当前任务实现。协程应使用 try/finally 释放资源,并在捕获 asyncio.CancelledError 后完成必要清理再重新抛出。这里的 acquire() 与 resource 代表项目中的异步资源获取与句柄操作:
| |
CancelledError 是 BaseException 的直接子类。结构化并发组件依赖取消工作;吞掉取消又不调用相应的取消状态处理,会造成任务组和超时行为异常。
超时也不等于底层操作一定已停止。线程中的阻塞函数不能被 Python 强行安全终止,远端请求也可能已经产生副作用。幂等性、底层超时和补偿策略仍需单独设计。
10. 背压与有界并发
一次创建百万个任务会消耗大量内存并压垮下游。使用有界队列或信号量限制在途任务:
| |
这里的 fetch() 代表异步 HTTP 客户端提供的请求函数。这个版本限制同时执行的 fetch 数量,但仍一次创建所有 Task。输入规模不受控时应进一步使用固定数量消费者和有界 asyncio.Queue,让生产者在队列满时等待,形成真正背压。
11. 如何做选择
可以按以下顺序判断:
- 先确认瓶颈是 CPU、I/O、锁竞争还是外部限流;
- 已有同步库且并发量适中,优先线程池;
- 整条调用链已有异步 API 且需要大量在途 I/O,考虑
asyncio; - 默认 CPython 下的纯 Python CPU 密集任务,考虑进程池;
- 任务可序列化且适合解释器隔离时,评估 3.14 的
InterpreterPoolExecutor; - 自由线程构建只有在依赖兼容、共享状态正确且基准受益时才采用;
- 无论哪种模型,都设置有界并发、超时、取消、错误聚合和关闭流程。
并发模型不是架构身份。一个服务可以在事件循环中管理网络连接,把阻塞调用交给线程,把大块 CPU 计算交给进程,但每跨一层都增加调度、序列化和故障处理成本。
12. 小结
- GIL 限制默认 CPython 中 Python 字节码的线程并行,不保证复合业务操作线程安全。
- Python 3.14 的自由线程构建已受支持但仍可选;扩展兼容和显式同步仍是前提。
- 线程适合同步阻塞 I/O,进程适合较粗粒度 CPU 任务,子解释器提供隔离的单进程多核方案,
asyncio适合协作式高并发 I/O。 - Python 3.14 不再在任何平台默认使用
fork;Linux 等平台通常使用forkserver,macOS 和 Windows 使用spawn,跨平台代码不要依赖隐含默认值。 - 取消、超时、有界并发和资源关闭是正确性的组成部分,不是上线前再补的优化。
下一篇将把代码组织成可测试、可构建的项目,统一 pyproject.toml、依赖和 CI。