Python 移除全局解释器锁(GIL)的尝试并不是突然出现的。过去二十多年里,社区提出过多种方案,从细粒度锁、替代解释器实验,到 Gilectomy 和后来的 nogil 分支。如今,自由线程构建把这项工作带进了主流 CPython,但“可以关闭 GIL”仍不等于“现有服务无需修改就能获得线性加速”。与此同时,uv 正从快速的依赖安装工具进入真实生产流程,团队需要同时审视性能、兼容性与构建可重复性。
为什么移除 GIL 一直很难
GIL 让同一进程中的 Python 字节码通常不会由多个线程同时执行。它限制了 CPU 密集型纯 Python 代码的线程并行能力,却也长期承担着重要的工程职责:简化 CPython 的对象内存管理,并让大量 C 扩展默认依赖一种相对简单的并发模型。
因此,历次尝试面对的并不只是“删除一把锁”,而是如何同时处理引用计数、对象生命周期、容器并发访问、扩展模块兼容性和单线程性能。早期细粒度锁方案证明了技术方向的可能性,也暴露了锁竞争和单线程回退;Gilectomy 等实验继续探索无 GIL 的 CPython;以 nogil 工作为基础的当前路线,则通过 PEP 703 推动可选 GIL 进入主流实现。
这次变化最重要的地方,是采用渐进迁移方式。自由线程构建可以与传统构建并存,库作者和应用团队能够逐步测试,而不是要求整个生态在同一天切换并发模型。
自由线程不自动等于更快
最可能受益的是 CPU 密集、任务可以拆分、并且主要执行 Python 代码的负载,例如并行解析、数值预处理或独立规则计算。对于网络请求、数据库访问等 I/O 密集型工作,传统线程已经可以在阻塞操作期间释放 GIL,关闭 GIL 后的收益可能有限。
还要关注几个边界:
- 某些扩展模块尚未适配自由线程,导入后可能无法使用,或者导致运行时重新启用 GIL。
- 过去“碰巧安全”的共享可变状态不能继续依赖 GIL,需要显式使用锁、队列或不可变数据。
- 更细的同步与新的内存管理机制可能增加开销,单线程任务未必变快。
- 基准测试必须覆盖吞吐量、尾延迟、内存和错误率,不能只比较一个循环的运行时间。
可以用下面的脚本检查解释器状态,并测试一个可拆分的 CPU 任务。运行前准备普通 CPython 与支持自由线程的 CPython 构建;不同发行方式的可执行文件名称可能不同,请替换命令中的解释器路径。
# benchmark_threads.py
import hashlib
import sys
import sysconfig
import time
from concurrent.futures import ThreadPoolExecutor
def cpu_task(seed: int, rounds: int = 250_000) -> bytes:
value = str(seed).encode()
for _ in range(rounds):
value = hashlib.sha256(value).digest()
return value
def run(workers: int) -> float:
started = time.perf_counter()
with ThreadPoolExecutor(max_workers=workers) as pool:
list(pool.map(cpu_task, range(8)))
return time.perf_counter() - started
print("Python:", sys.version.split()[0])
print("Free-threaded build:", bool(sysconfig.get_config_var("Py_GIL_DISABLED")))
if hasattr(sys, "_is_gil_enabled"):
print("GIL currently enabled:", sys._is_gil_enabled())
for workers in (1, 2, 4, 8):
print(f"workers={workers}: {run(workers):.3f}s")
python benchmark_threads.py
# 将 python3.13t 替换为本机自由线程解释器的实际命令
python3.13t benchmark_threads.py
这个样例只是烟雾测试。哈希库本身的实现细节可能影响结果,因此生产评估应换成服务中的真实任务,并至少进行预热和多轮采样。
把 uv 放进生产链路
uv 的生产价值不应只用“安装快”衡量。更关键的问题是:它能否让开发机、CI 与镜像构建使用同一份锁定结果,并在依赖漂移时直接失败。
在一个采用 pyproject.toml 的项目中,可以这样实践:
# 安装或更新依赖后生成锁文件
uv lock
# CI 中严格按 uv.lock 同步,锁文件过期时失败
uv sync --frozen
# 在锁定环境中执行测试和基准
uv run python -m pytest
uv run python benchmark_threads.py
容器构建可以把依赖元数据单独复制,利用缓存减少重复解析。以下是假设项目已提交 pyproject.toml 和 uv.lock 的最小示例,基础镜像与 uv 镜像标签应按团队的升级策略固定到经过验证的版本或摘要:
FROM python:3.13-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
COPY . .
RUN uv sync --frozen --no-dev
ENV PATH="/app/.venv/bin:$PATH"
CMD ["python", "-m", "my_service"]
这里使用 latest 只是为了让示例易于改造,生产环境不应依赖浮动标签。还要在 CI 中验证锁文件、目标平台 wheel、私有索引凭据和软件物料清单;如果需要自由线程解释器,则必须确认锁定依赖在对应 ABI 和平台上确实有可用构建。
建议采用双轨验证
自由线程 Python 与 uv 解决的是不同层面的问题:前者改变解释器的并发能力,后者强化环境和依赖管理。可以先让 uv 统一开发、CI 和容器中的安装流程,再为自由线程构建增加独立测试矩阵。
上线前应确认:传统解释器仍有回退通道;关键 C 扩展明确声明并通过自由线程测试;共享状态已经过并发审查;基准来自真实工作负载;镜像和 uv 版本固定;锁文件在 CI 中以冻结模式验证。这样才能把一次解释器实验变成可观测、可回滚的生产演进。