Python 3.13 无 GIL 实测:多线程终于跑得动了,但代价比想象中大

2026-10-07 0 188

公司内部有一个文档合规检查服务。功能不复杂:接收一批文档内容,逐条做正则匹配、关键词打分、敏感词比对、相似度计算,最后返回一份检查报告。全都是纯 Python 逻辑,不用 NumPy 也不用别的 C 扩展。

上线之后不久就遇到了瓶颈。单机只跑到一百多 QPS,CPU 有八个核,但只有一个核在满血跑,其他七个基本闲着。原因谁都懂——GIL。

我们当时上了 multiprocessing.Pool,开了八个工作进程,QPS 涨到八百多,问题看着解决了。但用了一段时间发现两个新问题:内存占用翻了几倍(每个进程都加载一份 Python 解释器和依赖),进程间传输数据的序列化开销在批量大文档的时候特别明显。

这就是 Python 生态里存在了三十年的老话题。终于在 CPython 3.13 里,官方提供了一个实验性的”自由线程”构建(free-threaded build),把 GIL 彻底关了。看到这个消息我第一时间就想试试——如果它能用,那我们的多进程方案就可以扔了。

两个星期下来,结论是:能用,但远没有宣传里那么香。这篇文章把整个过程记录下来。

先说清楚 GIL 是什么

GIL,全称是 Global Interpreter Lock,全局解释器锁。它是 CPython 实现里的一把互斥锁,保证任意一个时刻,只有一个线程在执行 Python 字节码。

它不是 Python 语言规范的一部分,而是 CPython 这个具体实现的产物。原因很历史——CPython 的内存管理用引用计数,引用计数的增减必须原子化,最省事的实现方式就是一把大锁把所有字节码执行串起来。

带来的直接后果是:哪怕你的机器有 32 个核,Python 的多线程也跑不出并行的效果。IO 密集型的场景还能受益(因为 IO 操作时线程会释放 GIL),CPU 密集型的场景则完全无感,甚至更慢(切换线程还有开销)。

绕开它的常用办法是 multiprocessing。多进程确实是真并行,但代价也不小:进程启动慢、内存占用大、数据要序列化传输、共享状态复杂。对于短任务、小批量的场景,得不偿失。

所以当官方说”CPython 现在可以编译成无 GIL 的版本”的时候,整个社区是兴奋的。这意味着一个进程内可以用多线程真并行了。

怎么装一个自由线程的 Python

官方从 3.13 开始提供自由线程构建。安装方式有几种。

最简单的办法是用 uv,它是目前装 Python 版本最省心的工具:

# 装一个 3.13 的自由线程版本
uv python install 3.13t

# 建一个虚拟环境
uv venv --python 3.13t

# 激活
source .venv/bin/activate

# 验证一下
python -c "import sys; print(sys.version)"

注意 3.13t 里的 t 是 “threaded” 的意思,是官方约定的后缀。

也可以直接从 python.org 下载。macOS 和 Windows 的安装包里都有 “Free-threaded” 这个选项。

装好之后,验证两个东西。第一个是构建类型:

import sysconfig
print(sysconfig.get_config_var("Py_GIL_DISABLED"))
# 1 表示是自由线程构建
# 0 或 None 表示普通构建

第二个是运行时 GIL 是否真的关掉了:

import sys
print(sys._is_gil_enabled())
# False 表示 GIL 已关闭
# True 表示 GIL 还在

这里有个容易忽略的细节:自由线程构建默认还是会启用 GIL。只有通过环境变量 PYTHON_GIL=0 或者在启动时显式关闭,GIL 才会真的消失。这样做是为了让第三方扩展可以先兼容上,再逐步过渡。

PYTHON_GIL=0 python app.py

我们跑压测的时候都是开着 PYTHON_GIL=0 跑的。

被测的服务长什么样

服务本身逻辑不复杂,但确实是纯 Python 的 CPU 密集。简化版大概是这样:

import re
from dataclasses import dataclass

SENSITIVE = {f"keyword_{i}" for i in range(500)}

@dataclass
class CheckResult:
    doc_id: int
    score: float
    hits: list

def check_one(doc_id: int, content: str) -> CheckResult:
    # 1. 分词(用正则切)
    tokens = re.findall(r'[a-zA-Zu4e00-u9fa5]+', content)

    # 2. 敏感词命中
    hits = [t for t in tokens if t in SENSITIVE]

    # 3. 关键词打分(纯 Python 循环)
    score = 0.0
    for t in tokens:
        h = 0
        for ch in t:
            h = (h * 131 + ord(ch)) % 2147483647
        score += h % 100 / 100

    # 4. 长度惩罚
    if len(content) < 50:
        score *= 0.5
    if len(content) > 5000:
        score *= 0.8

    return CheckResult(doc_id, score, hits)

这个函数里有大量纯 Python 的字符遍历和整数运算,没有调用任何 C 扩展。这是为压测专门做的隔离——如果中间夹着 NumPy 之类的 C 库,它们的内部计算本来就会释放 GIL,那么线程并行的效果就会被混淆,测的不是 GIL 的影响。

数据是一批从线上脱敏导出的文档片段,平均每篇 800 个字符,一共 2000 篇。

四组对比实验

测了四种实现,分别在不同的 Python 构建上跑。

实现 A:单线程顺序处理

def run_serial(docs):
    return [check_one(did, text) for did, text in docs]

实现 B:8 进程 multiprocessing

from multiprocessing import Pool

def run_multiprocess(docs, workers=8):
    with Pool(workers) as p:
        return p.starmap(check_one, docs)

实现 C:8 线程 ThreadPoolExecutor

from concurrent.futures import ThreadPoolExecutor

def run_threads(docs, workers=8):
    with ThreadPoolExecutor(workers) as ex:
        return list(ex.map(lambda d: check_one(*d), docs))

这台测试机是 8 核 16 线程的 AMD Ryzen。测试跑三轮取中位数。

结果

环境 实现 总耗时 相对单线程加速
3.13 普通构建 A 单线程 42.8s 1.0x
3.13 普通构建 B 多进程(8) 6.2s 6.9x
3.13 普通构建 C 多线程(8) 43.5s 0.98x
3.13 自由线程构建 A 单线程 49.1s 0.87x
3.13 自由线程构建 B 多进程(8) 7.5s 5.7x
3.13 自由线程构建 C 多线程(8) 7.0s 6.1x

这张表里藏着三件事。

第一:普通构建下多线程毫无意义。42.8 秒对 43.5 秒,多线程甚至慢了一点点。这就是 GIL 的实锤。

第二:自由线程构建下多线程真的能并行了。从 49.1 秒降到 7.0 秒,加速 7 倍。这是过去无法想象的事情。

第三,也是最重要的:单线程性能掉了一大截。普通构建 42.8 秒,自由线程构建 49.1 秒,慢了 14.7%。这是官方文档明确写到的代价,但看到真实数字还是有点意外。

为什么单线程会变慢

官方给的原因主要在两点。

一是引用计数的实现变复杂了。多线程环境下要让引用计数的增减原子化,不能再用一把大锁,得用更精细的机制,比如”偏置引用计数”(biased reference counting)。这套机制的每条路径都比原来长一些,单线程执行下来累积起来就是明显的开销。

二是为了兼容 GC 和对象生命周期,做了不少额外检查。比如”不朽对象”(immortal objects)这套机制,本来是给自由线程准备的优化,但它本身在单线程下也引入了一点开销。

官方说这个差距在 3.13 里是”5% 到 15%,视工作负载而定”。我们实测的 14.7% 落在了上边界附近,因为这个工作负载字符循环特别多,正是开销最敏感的路径。

对我们来说,单线程慢 15% 是可以接受的,因为我们本来就不打算用单线程跑这个服务。但如果是”平时用单线程就够了,偶尔想让多线程加个速”的场景,那么切到自由线程构建之后,平时会变慢,偶尔加速。这笔账得算清楚。

有意思的发现:自由线程下多进程也变慢了

看表里那一行特别有意思的地方:自由线程构建下,8 进程方案的耗时是 7.5 秒,比普通构建下的 6.2 秒慢了 21%。

原因也很清楚。自由线程构建的单线程性能本来就慢,多进程方案本质上还是”每个进程跑一份单线程逻辑”,所以每个子进程都跟着变慢了。

这说明一件事:自由线程构建没有免费午餐。你在多线程上赚到的,是拿单线程性能换来的。如果你的方案本来就用多进程跑得很好,切到自由线程构建不仅没好处,还会整体变慢。

反过来,如果你的场景是”短任务、数据没法很好分进程、进程启动开销占比大”,那么自由线程的多线程方案会比多进程更合适,因为省掉了进程启动和序列化的成本。这种情况下即使单线程性能掉一点,综合下来还是赚的。

C 扩展的兼容性:真实的拦路虎

上面那个服务是纯 Python,所以能跑起来。但绝大多数实际项目都依赖 C 扩展,这里才是自由线程最麻烦的地方。

我们在另一个项目里试过。这个项目依赖:NumPy、pandas、SQLAlchemy、psycopg2-binary、cryptography、Pillow。安装到自由线程 Python 里各试了一遍。

库 自由线程构建安装结果
NumPy 已经提供 FT 版本 wheel,安装即用
pandas 同上
Pillow 同上
SQLAlchemy 纯 Python,安装没问题
cryptography 早期版本装不上,需要升级到指定版本
psycopg2-binary 装不上,只能换成纯 Python 的 psycopg3

结论是:生态在赶上来,但还没到”装什么都能用”的程度。Python 3.13 刚出的时候 NumPy 还没跟上,需要等几个月。现在好很多,但边缘一点的库还是有空缺。

真正的坑不是装不上,而是同一个 C 扩展在多线程并行的场景下有没有正确实现线程安全。很多扩展的作者当时写代码的时候默认有 GIL 兜底,从来没考虑过并发问题。自由线程构建下它们虽然能跑,但可能会在特定场景下产生难以复现的 bug。

这一点官方也承认。3.13 的自由线程构建被明确定位为”实验性”,不支持生产环境。3.14 会进一步稳定,但真正能上生产,社区普遍估计要到 3.16 左右。

什么东西应该先试,什么东西先别碰

如果你也想试试,我给点建议。

可以试试的场景:

  • 纯 Python 的 CPU 密集任务,比如自定义的文本处理、规则引擎、算法计算
  • 短任务、批量大的场景,多进程启动成本占比高
  • 内部工具、离线批处理任务,不面向真实用户
  • 新项目,从第一天起就能控制依赖范围

别碰的场景:

  • 线上核心服务。3.13 的自由线程构建官方就说了是实验性,出问题没人为你负责。
  • 依赖复杂 C 扩展的场景。特别是小生态里的库,很可能还没有 FT 版本。
  • IO 密集型服务。这种场景 GIL 本来就不是瓶颈,多线程已经能跑得很好,没必要换。
  • 已经用多进程跑得很顺的服务。切换过去的收益不确定,风险倒是确定有。

该怎么判断一个服务要不要切

我把判断逻辑总结成了一个简单的问题清单。

第一问:这项任务是 CPU 密集的吗?如果大部分时间在等 IO,切过去没有任何好处。IO 密集的多线程早就并行了。

第二问:当前是不是卡在多线程跑不动上?如果现在已经在用多进程,跑得挺好,那切过去的唯一收益就是省掉序列化开销和进程启动时间。这个收益得大到能盖过单线程 15% 的性能损失才值得。

第三问:依赖能装得上吗?先不管别的问题,把 uv sync 在 FT Python 里跑一遍,有任何一个装不上就先放弃。

第四问:能接受”实验性”这个标签吗?包括可能的内存问题、可能的多线程数据竞争、可能某天跑着跑着 segfault。如果这是一个不能出错的服务,答案就是”不能”。

我们那个合规检查服务,四问下来其实都是”可以”,因为它逻辑独立、没有第三方依赖、跑在内部环境里。但最后我们也没切,因为多进程方案跑得好好的,切换只是把进程间通信换成了线程间共享内存,省了点内存和启动开销,收益不足以承担实验性风险。

这次试完,我的态度是”很兴奋,但先等等”。

未来会怎么样

自由线程这件事的实现路径,社区讨论了好几年,三次尝试失败之后终于在 3.13 落地,这是很大的里程碑。但要等它真正稳定、生态适配完整、性能代价降下来,还有不短的路要走。

根据 PEP 703 和官方的路线图,大致的节奏是:

  • 3.13:实验性引入,开发者可以开始适配
  • 3.14:性能改善,C 扩展适配比例提升,仍然实验性
  • 3.15 / 3.16:可能开始提供”稳定”标签,允许在受控环境里使用
  • 3.17 及以后:成为默认构建的可能性开始被讨论

这是乐观估计。实际进度会受很多因素影响,特别是生态的适配速度——几万个 C 扩展库,不可能所有人都主动改。

另一条并行的路线是 JIT。CPython 3.13 也引入了实验性的 JIT(基于 copy-and-patch 技术),官方给的定位是”性能会先降后升”。这两条路线未来会合流,最终带来一个”既没有 GIL、又比现在快”的 CPython。但现在都还早。

写在最后

这次实测最有价值的收获其实不是”自由线程好不好用”,而是让我把 Python 并发这件事底层的取舍看得更清楚了。

没有 GIL 不等于”免费多线程”。你要为此付出单线程性能的代价,要付出 C 扩展适配的成本,要付出生态成熟的等待时间。这些都是 GIL 在三十年前被引入时用”简单”换来的”稳定”,现在要重新权衡这笔交易。

对我们这些写业务代码的普通工程师来说,最实际的态度是:关注它,理解它,但不着急用它。等到 3.15、3.16 的时候再看一次,那时候的生态和性能都值得重新评估。在那之前,用好多进程,把任务合理地切分,仍然是 Python 并发场景里最稳的方案。

毕竟技术选择永远是权衡。当”更快”要拿”更不稳”来换的时候,答案往往取决于你在意的是哪一个。

Python 3.13 无 GIL 实测:多线程终于跑得动了,但代价比想象中大
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 python Python 3.13 无 GIL 实测:多线程终于跑得动了,但代价比想象中大 https://www.taomawang.com/server/python/2903.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务