并行的 Python 执行线程并排运行在一层严格类型检查的安全层之上

自由线程 Python 来了:为什么类型检查现在更重要

自由线程 Python 不再是一个实验。自 2025 年 10 月 7 日发布的 Python 3.14 起,自由线程(无 GIL)构建在 PEP 779 下获得正式支持,而非实验性(Python 3.14 发布说明,python.org)。如果过去几年你一直半留意着"无 GIL"这个故事,那么现在就是它成真的时刻。

我们认为这是 Python 自类型提示落地以来最大的变化之一,而它有一个没有足够多人在谈论的直接后果:当你的线程真正并行运行时,类型错误的代价会上升。 那么让我们梳理一下改变了什么、它为何提高了风险,以及你今天能为此做些什么。

Python 3.14 究竟改变了什么

在 Python 大部分生命周期里,全局解释器锁(GIL)意味着同一时刻只有一个线程执行 Python 字节码。你可以编写多线程代码,但 GIL 会悄悄地把它串行化。这在实践中让一整类并发 bug 变得罕见,因为两个线程几乎从不会在完全相同的瞬间触碰同一个对象。

PEP 703 于 2023 年 10 月 24 日被 Python 指导委员会接受,提议通过 --disable-gil 构建让 GIL 变为可选。Python 3.13 以实验性的形式发布了该构建。Python 3.14 完成了这项工作:根据官方 3.14 发布说明,"PEP 703 中描述的实现已经完成,包括 C API 的变更",而单线程性能损失现在"约为 5-10%,取决于平台和所用的 C 编译器"。

其推出遵循三个阶段,如 PEP 779 所述:

  • 第一阶段(Python 3.13): 自由线程构建可用,但明确为实验性。
  • 第二阶段(Python 3.14): 自由线程构建获得正式支持,但为可选。
  • 第三阶段(未来): 自由线程构建成为默认。

我们现在正处于第二阶段。那个"正式支持"的标签,正是改变你应当如何看待自己代码的那一部分。

为什么真正的并行提高了类型错误的风险

接下来是令人不安的部分。当线程真正在同一时刻运行时,其中两个可以并发地读写同一个对象。官方的自由线程 HOWTO 谨慎地说明了解释器为你保证什么、又不保证什么:

"Python 在历史上从未为对这些内置类型的并发修改保证特定行为,因此这应被视为对当前实现的描述,而非对当前或未来行为的保证。"

而它的建议很直接:

"在可能的情况下,建议使用 threading.Lock 或其他同步原语,而不是依赖内置类型的内部锁。"

所以在真正并行下的正确性是你的工作,而非解释器的。现在想想类型错误在那样的世界里会做什么。假设一个函数认为某个共享值是 list,而另一个把它当作 dict。在旧的 GIL 下,这种不匹配也许会表现为偶发的、可复现的异常。在自由线程下,同样的不匹配现在可以与第二个线程的访问交错,变成糟糕得多的东西:一个损坏的结构、一个静默的错误答案,或者一个只在负载下出现、在你的笔记本上永远无法复现的崩溃。

类型错误一直都是 bug。并行只是移除了曾经用来掩盖它的缓冲。一个错误的类型在你的程序中流动,正是那种在 GIL 停止为你串行化一切的那一刻会变得更危险、而非更安全的缺陷。

我们想在这里对你精确一些,因为这正是很多工具营销变得草率的地方。类型检查器不检测数据竞争。 Basilisk 没有线程规则、竞争检测或锁分析器,我们也不会假装它有。类型检查器所做的,是在你的程序运行之前,消除整整一类"这个值不是这段代码所假设的类型"的问题。在一个自由线程的世界里,这一类正是你那些最糟糕、最难复现的 bug 中出人意料地大量来源。提前把它封住是廉价的。在生产环境里调试一个海森堡 bug 则不然。

类型提示已经可选了十年。这才是更深层的问题。

类型提示随 PEP 484 于 2015 年在 Python 3.5 中到来。十年过去,大多数 Python 开发者都会写它们。但写一个类型提示和强制执行它是两件截然不同的事,而强制执行的那一半,正是团队悄悄失守的地方。

大多数 Python 类型检查器都把严格性当作选择加入的。有一个标志,或一个模式,或一个你本该开启的配置项,而在许多真实的代码库里从来没有人开启它。于是你最终得到一些看起来像安全网、却什么都捕获不到的注解,因为检查器运行在一种让未类型化和错误类型化的代码溜过去的模式下。

在 GIL 为你打掩护的时候,这就已经是个问题了。在一个自由线程的世界里,它是个更大的问题,因为那些注解本应捕获的 bug 现在有了更锋利的牙齿。

你今天能做什么

你不需要等到第三阶段,也不需要重写任何东西就能抢先应对。几个具体的行动:

  1. 默认开启类型检查,而不是作为一个可选标志。 我们见到的最常见的失败,是一个拥有漂亮类型提示、检查器却运行在忽略其中大多数的宽松模式下的项目。如果严格性是某个队友必须记得去开启的东西,它就会被忘记。
  2. 在 CI 中、在每一个拉取请求上检查类型。 大约 30% "总是"或"经常"使用类型提示的开发者,其 CI 管道中仍然根本没有类型检查(Typed Python 2024 开发者调查,Meta 与 Microsoft)。一个从不被检查的提示只是一句注释。
  3. 对真正共享的状态遵循自由线程指南。 使用 threading.Lock 或另一种同步原语,而不是依赖解释器的内部锁,正如官方 HOWTO 所建议的那样。静态类型和真正的锁解决的是不同的问题,在自由线程下你两者都需要。

Basilisk 处在哪里

Basilisk 是我们对"强制执行是可选的"这个问题的回答。它是一个用 Rust 构建的开源、严格优先的 Python 类型检查器和语言服务器,其默认行为就是 Python 类型规范,开启所有符合性规则,没有要忘记的 --strict 标志。

那个默认是可测量的。Basilisk 在官方 python/typing 符合性套件上取得 100%,而这个数字不是自我报告的:它来自套件自己未经修改的评分工具对 Basilisk 开箱即用配置的评分,也就是给那个页面上其他所有检查器评分的同一套工具。当我们说严格是默认时,这就是凭据。

几条诚实的边界,好让你确切知道你得到的是什么:

  • Basilisk 的规范目标是 Python 3.12。它尚未对 Python 3.14 的语法做特殊处理,例如 PEP 649 与 PEP 749 的延迟注解求值。本文的类型安全论点是关于你的代码在真正并行下的正确性,无论你运行哪个较新的解释器,它都成立。
  • Basilisk 没有针对并发的专门分析。 它的工作是捕获类型错误,而这是那种在 GIL 不再为你串行化程序之后变得更有价值的通用防线。
  • 在符合规范的默认之外,一小组更严格的房屋风格规则在你需要时只差一次配置改动:要求每个参数都有类型(BSK-E0001)、每个返回值都有类型(BSK-E0002),在覆盖基类方法时要求 @overrideBSK-E0025),标记冗余注解(BSK-W0050),并对显式 Any 予以提示(BSK-W0014)。它们默认关闭,并按项目限定。

它作为单个二进制文件发布,没有运行时依赖,而一个扩展就能在 VS Code、Cursor、Zed 和 Neovim 中为你提供完整的工作流:悬停、跳转到定义、自动补全、重构、集成调试和性能分析。

底线

自由线程 Python 是一件真正的好事。真正的并行是这门语言想要了很久的东西,而 3.14 团队赢得了这个里程碑。但每一个强大的特性都会带来一笔账单,而真正并行的账单是正确性纪律。解释器已经明确告诉你,并发正确性现在有一部分是你的责任。

静态类型检查不会替你写锁。它会做的,是在整整一类 bug 有机会在凌晨三点、在生产环境里跨两个线程糟糕地交错之前,就把它们删除。这一直都是值得做的。在一个自由线程的世界里,它是你能买到的最廉价的保险之一。

如果你想要默认开启、而不是一个你必须记住的标志的严格性,来试试 Basilisk

常见问题

自由线程 Python 获得正式支持了吗?

是的。自 2025 年 10 月 7 日发布的 Python 3.14 起,自由线程(无 GIL)构建在 PEP 779 下获得正式支持,脱离了它在 Python 3.13 中的实验性状态(Python 3.14 发布说明)。

移除 GIL 会让我现有的代码变得不安全吗?

移除 GIL 不会改变你的代码,但它移除了常常让并发 bug 无法浮现的串行化。官方自由线程 HOWTO 建议对共享状态使用 threading.Lock 或其他同步原语,而不是依赖解释器的内部锁,并指出内置类型的并发修改行为"不是对当前或未来行为的保证"。

类型检查器能检测数据竞争吗?

不能。静态类型检查器,包括 Basilisk 在内,都不检测数据竞争或分析加锁。它所做的是消除类型错误,而这是一类在真正并行下变得更危险的 bug。静态类型和运行时同步原语解决的是不同的问题,在自由线程下你两者都需要。

自由线程 Python 的性能代价是多少?

根据 Python 3.14 发布说明,自由线程模式下单线程的性能损失现在"约为 5-10%,取决于平台和所用的 C 编译器",相较早期构建有显著改进。

Basilisk 在这一切中如何提供帮助?

Basilisk 是一个严格优先的 Python 类型检查器,其默认行为就是完全符合 Python 类型规范,没有要忘记的严格标志。它在官方 python/typing 符合性套件上取得 100%(由套件自己的评分工具测量),因此类型错误开箱即用就会被捕获,而不是只有在有人记得开启更严格的模式时才被捕获。