自由线程让多个线程能够真正并行执行 Python 代码,但移除全局解释器锁并不等于并发编程自动变得简单。Tobias Wrigstad、Fridtjof Stoldt 和 Donghee Na 在 Python Language Summit 2026 提出,应为自由线程 Python 建立一种兼顾安全、性能与可用性的高层并发模型。关键问题已经从“线程能不能并行”转向“开发者如何在不制造数据竞争的前提下使用并行能力”。
解除 GIL 只是运行时能力,不是完整编程模型
传统 CPython 中,GIL 在很多场景下限制了 Python 字节码的并行执行,同时也意外掩盖了一部分共享状态问题。进入自由线程环境后,两个线程可能同时读取和修改同一对象,过去“看起来没出错”的代码不再值得信任。
例如,counter += 1 并不是一个应被视为原子事务的业务操作。更复杂的风险通常藏在跨对象不变量中:
- 一个字典保存账户余额,另一个集合保存冻结账户;
- 一个列表保存任务,另一个计数器记录未完成数量;
- 检查缓存不存在某个键后,再创建并写入该键;
- Python 对象与原生扩展共享同一块可变内存。
即使单个容器操作有内部保护,也不代表一组操作组成的业务规则是原子的。因此,高层并发模型不能只是增加几把锁。它需要让任务边界、数据所有权、错误传播和生命周期在代码结构中清晰可见。
来源摘要只说明了这是一项“安全且高性能的高层并发模型”提案,并未给出最终 API。因而不宜提前假设具体类名或语法,但可以用几个问题判断未来设计是否有效:
- 共享是否显式:开发者能否一眼分辨局部数据、只读数据和跨任务可变数据?
- 生命周期是否有边界:父任务结束时,子任务是否已经完成、取消或被明确移交?
- 失败是否可组合:多个并发任务失败时,异常是否会被完整收集,而不是静默丢在线程中?
- 安全是否低成本:正确写法能否同时成为常见场景下的高性能写法?
- 旧生态能否迁移:现有线程库、C 扩展和框架是否有渐进式升级路径?
更可靠的方向:少共享,明确所有权
对业务开发者而言,最有价值的抽象通常不是“线程对象”,而是“并发任务处理什么数据,以及结果交给谁”。一种稳健模式是:
- 调度者创建任务并拥有任务集合;
- 每个工作任务只修改自己的局部状态;
- 输入尽量使用不可变值或独占数据;
- 工作任务返回不可变结果;
- 汇总者在单一位置更新最终状态。
这种模式不能消除所有同步,但能把共享可变状态压缩到少数、可审计的边界。锁仍然有用,只是不应成为默认的数据组织方式。
高层模型还应处理结构化并发问题。调用方启动十个任务后,不应在不知情的情况下留下三个后台线程继续操作已经失效的资源。超时、取消、部分失败和资源释放需要与任务作用域绑定。否则,自由线程提供的性能越高,失控任务制造问题的速度也可能越快。
用现有 Python 写出适合自由线程的任务边界
下面的示例不是峰会提案的假定 API,而是一种今天就能运行、也便于未来迁移的组织方式。它使用线程池处理 CPU 任务,但不让工作线程共同修改一个计数器或字典。
将代码保存为 parallel_jobs.py:
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
import os
@dataclass(frozen=True, slots=True)
class Result:
job_id: int
total: int
def cpu_task(item: tuple[int, int]) -> Result:
job_id, limit = item
# total 只属于当前任务,不与其他线程共享。
total = 0
for number in range(limit):
total += (number * number + 3 * number) % 97
# 返回不可变结果,而不是修改共享字典。
return Result(job_id=job_id, total=total)
def main() -> None:
limits = [1_000_000, 1_100_000, 1_200_000, 1_300_000]
worker_count = min(len(limits), os.cpu_count() or 1)
with ThreadPoolExecutor(max_workers=worker_count) as pool:
results = list(pool.map(cpu_task, enumerate(limits)))
# 只有协调线程负责最终聚合。
totals = {result.job_id: result.total for result in results}
print(totals)
print("grand total:", sum(totals.values()))
if __name__ == "__main__":
main()
运行:
python parallel_jobs.py
这段代码在常规 CPython 上也能正确运行,但纯 Python CPU 循环未必因线程而加速;要观察自由线程带来的 CPU 并行收益,需要使用支持自由线程的 Python 构建,并结合目标版本的启动方式进行验证。不要把这个小程序当成性能基准,正式评估时应测试真实负载、不同任务粒度和线程数量。
该示例值得保留的不是 ThreadPoolExecutor 本身,而是三个设计决定:工作线程不写共享容器、结果不可变、聚合集中在一个所有者中。如果未来高层模型提供任务组、隔离对象或更强的所有权约束,这种代码通常更容易迁移。
采用自由线程前的检查清单
团队不应只比较“开启前后快了多少”,还要审查并发边界:
- 找出模块级字典、缓存、单例和可变类属性;
- 检查“先判断、后修改”这类跨多步操作;
- 确认依赖的原生扩展明确支持自由线程;
- 为取消、超时和多个任务同时失败设计测试;
- 使用压力测试和竞争检测手段,而不只运行普通单元测试;
- 控制线程池数量,避免与数据库连接池、BLAS 或其他内部线程池叠加后过度订阅 CPU;
- 同时记录吞吐量、尾延迟、内存占用和同步开销。
自由线程解决的是并行执行能力,高层并发模型要解决的则是开发者如何安全地使用这种能力。真正成熟的方案,应让默认路径倾向于局部状态、清晰所有权和有边界的任务,而把任意共享可变对象变成需要主动选择、能够审计的例外。