自由线程Python:过去、现在与未来
摘要
Thomas Wouters 在 PyCon US 2026 上发表了关于自由线程 Python 的过去、现在与未来的演讲,该 Python 版本移除了全局解释器锁 (GIL),允许并行线程执行。
<p><a href="https://lobste.rs/s/ekeur9/free_threaded_python_past_present_future">评论</a></p>
查看缓存全文
缓存时间: 2026/06/25 07:10
# 自由线程 Python:过去、现在与未来
来源:https://lwn.net/SubscriberLink/1078367/eaa511915870fdb2/
过去五年左右,Python 可能经历的最大变革是“自由线程”版本的出现,该版本移除了全局解释器锁(GIL),允许多个线程在解释器中并行运行。
在五月中旬于加利福尼亚州长滩举办的 PyCon US 2026 (https://us.pycon.org/2026/) 上,长期担任 CPython 核心开发者(同时也是现任指导委员会成员)的 Thomas Wouters 发表了一场关于该特性的演讲。他探讨了移除 GIL 背后的动机、一些历史背景、自由线程解释器的当前状态,并对未来的发展方向做出了预测。
> LWN.net 能够为您带来像本文这样的文章,全靠我们慷慨的订阅者。如果您希望看到更多类似内容,请考虑利用我们的特别优惠:1 个月试用订阅 (https://lwn.net/Promo/daroc1/claim)
他首先指出,自己在 CPython 核心开发方面已有大约 25 年的经验,并在过去六年中的五年里担任指导委员会成员。指导委员会负责决定语言特性的前进方向,包括自由线程。除此之外,他在 Meta 公司从事自由线程解释器及其他相关工作。虽然与演讲内容并非完全相关,但他提到自己有三只猫,并展示了相关幻灯片。“在另一个平行宇宙里,有一版演讲我会用我的猫来当幻灯片”,他的话引起了笑声和掌声。
#### 动机
不过,他接着说,“这不是那个演讲”,“这是一个无聊的演讲”。虽然他觉得大部分听众可能已经了解,但他还是简要介绍了线程。线程提供了一种方式,能够在单个进程及其地址空间内,通过独立的“控制线程”同时执行多个任务。线程存在的主要原因是为了性能;内存访问很慢,因此线程是一种在等待内存时让 CPU 有事可做的方法。
[![[Thomas Wouters]](https://static.lwn.net/images/2026/pycon-wouters2-sm.png)](https://lwn.net/Articles/1078544/)
使用多进程也能获得类似的好处,但开销更大,而且地址空间不共享。除了性能之外,像 Python 这样的语言需要线程还有一些其他原因。例如,在调用阻塞 API 或与需要使用线程才能访问的第三方库(“通常是数据库”)交互时,继续执行主程序。
GIL 是“CPython 很久以前决定支持线程的方式”,其历史早于他参与该语言开发的时间。GIL 保护 Python 对象及其引用计数(用于判断对象是否仍在使用);它也保护 CPython 的内部实现,并且是“CPython 支持线程的最有效方式”。GIL 并不保护用户的 Python 代码,因为用户无法控制解释器何时释放和重新获取 GIL。它“基本上不保护”C 和 C++ 扩展,尽管扩展调用何时可能释放 GIL 更明确,“但仍然很容易在无意中依赖 GIL,最终导致不安全”。
“从根本上说,线程很难”;它们很复杂,GIL 并没有让它们变得更简单,Wouters 说道,尽管有时看起来是这样。但 GIL 也使得线程不那么有用,这就是多年来一直有努力将 GIL 从 Python 中移除的原因。当 GIL 首次被加入时,多 CPU 系统还很罕见;而现在他的手机都有八个核心。
长期以来,另一种选择是用 C 重写部分程序,尽管在过去几年中,这一选择已转向 Rust(它更好)。然而,这种答案并非总是有效;它意味着需要对应用程序的大部分进行重写,不仅要转换代码,还要将数据切换到该语言的处理域中。
还有其他选项,例如使用多进程,但这通常需要更多内存以及在进程之间复制数据。**子解释器 (https://lwn.net/Articles/941090/)** 允许单个进程拥有多个解释器,每个解释器都有自己的 GIL,可以在独立的线程中运行,但使用这种方法存在一些问题。与第三方库的交互(尤其是那些并非为 Python 设计的库)“尚未就绪”,而且到目前为止,在子解释器之间复制数据还没有好的方法。还有 **asyncio (https://docs.python.org/3/library/asyncio.html)**,其本质上与**绿色线程 (https://en.wikipedia.org/wiki/Green_thread)** 是相同的概念。他是 asyncio 的大力支持者,并认为它是处理网络 I/O 的最佳方式,“而使用线程很容易出错”。
但有时这些解决方案无法提供所需的性能,他说道。“没有 GIL,针对这些问题的多线程解决方案可以提供更高的吞吐量、更低的内存使用和更低的延迟。” 但是,如前所述,线程很难。这是因为数据共享很困难。CPU 和编译器为了速度进行优化,由于内存访问很慢,它们会执行各种操作,如缓存、预取数据和重排内存访问。
这些技术与线程之间的交互“极其复杂”。如果一个线程写入一个值,另一个线程何时才能看到它,并且看到的是完整的值,而不是写入内容的一半?如果一个线程写入了两个东西,另一个线程看到它们的顺序是怎样的?处理这些问题需要内存屏障、原子操作和内存模型等机制,他不打算深入讲解。他的结论是:“线程中共享的可变数据很糟糕。”
对于 Python 来说,问题在于一切都是对象——并且一切都是共享的。每个对象都有一个引用计数,这是每个对象中一小部分可变数据,“所以对于 Python 来说,一切都是共享的可变数据”。Wouters 表示,CPython C API 以多种方式依赖 GIL。例如,`PyDict_GetItem() (https://docs.python.org/3/c-api/dict.html#c.PyDict_GetItem)` 返回一个对字典中项的**借出引用 (https://docs.python.org/3/glossary.html#term-borrowed-reference)**,如果 GIL 确保对象在代码(例如增加引用计数以拥有自己的(非借出)引用)执行期间不会发生变化,那么这没有问题。如果没有 GIL,对象随时可能发生变化,尽管可能不会频繁引发问题,但更改引用计数的代码是依赖 GIL 来保证正确性的。
锁可以用来避免这类问题,但它们开销很大,并且需要到处使用。Python 有列表、字典、元组、代码对象等等,它们“无处不在,整个进程到处都是”;用锁来替代引用计数将“极其昂贵”。即使只是让 `PyDict_GetItem()` 使用锁也会很昂贵,而且锁会存在于所有 Python 代码中,即使代码没有使用线程。“我们不能为了让多个线程能同时执行而将解释器速度减慢 50%”,尤其是目前没有用户用多线程运行 Python 工作负载,“因为他们做不到”。
#### 历史
移除 GIL 的努力最早可以追溯到 **Greg Stein 于 1996 年为 Python 1.4 编写的补丁 (https://dabeaz.blogspot.com/2011/08/inside-look-at-gil-removal-patch-of.html)**;它用细粒度锁替代了引用计数,并添加了其他锁到 CPython 的各个部分。这个方案没有成功,因为单线程性能损失不可接受。
2013 年,Trent Nelson 启动了 **PyParallel (https://pyparallel.org/)**,这是一个完全不同的模型,不使用操作系统线程。它提供了一个不同的 API,用于在 Python 内部创建线程来执行各种任务;它会延迟引用计数和垃圾回收,直到这些线程完成。这不是一个通用的解决方案,因为它无法与任何已经使用操作系统线程的东西一起工作。
2015 年,Larry Hastings 启动了 **Gilectomy 项目 (https://lwn.net/Articles/689548/)**,他为此工作**了几年 (https://lwn.net/Articles/754577/)** (https://lwn.net/Articles/723514/)。他研究了处理引用计数的新颖方法,但也研究了从语言中移除 GIL 还需要哪些其他工作。他“基本放弃了”,尽管还有一些他想要探索的途径,Wouters 说道。
几年前,“在 Meta 工作的 Sam Gross 认为他*能够*解决大部分问题,并且他确实做到了”,在 **CPython 3.9 的无 GIL 分支 (https://lwn.net/Articles/872869/)** 中实现了这一点。“这就是我们现在所拥有的,或多或少。” Gross 撰写了 **PEP 703 (https://peps.python.org/pep-0703/)**(“在 CPython 中使全局解释器锁成为可选”),其中描述了在 CPython 中支持自由线程并移除 GIL 所需的各种技术。
例如,Python 中的对象现在有一个“拥有者”线程 (https://peps.python.org/pep-0703/#biased-reference-counting),这意味着该线程可以对对象及其引用计数进行快速访问;其他线程仍然可以访问该对象,但必须走一条较慢的路径,该路径在单独的共享引用计数上使用原子操作。此外,**某些引用计数操作会被推迟 (https://peps.python.org/pep-0703/#deferred-reference-counting)** 到垃圾回收时,然后一次性处理多个操作;然而,这构成了语义上的变化,因为那些在计数归零时本应立即回收的对象将被延迟到下一次垃圾回收运行。“这是一个很小的差异,可能是可以接受的。”
还有 **在访问列表和字典成员时的推测性引用计数操作 (https://peps.python.org/pep-0703/#optimistic-avoiding-locking-in-dict-and-list-accesses)**。一个项对象的引用计数会被增加,然后 CPython 会检查该项是否仍然存在于列表或字典中;“这听起来真的很奇怪”,并且可能很危险,因为对象可能已经被销毁了,他说道。有保护措施,并且在受控的条件下进行;这不是用于一般用途,而是“解释器内部的魔法”。
“基于静止状态的回收”用于确保对象不会在其他线程的脚下消失。当列表和字典被释放时,它们的内存不会立即被重用,以便其他线程仍然可以访问对象上的引用计数 (https://peps.python.org/pep-0703/#optimistic-dict-and-list-access-summary)。只有当语言知道这样做是安全的时候,内存才会被清除,这通常是在进行垃圾回收时,因为此时所有线程都达到了静止状态。
[![[Thomas Wouters]](https://static.lwn.net/images/2026/pycon-wouters-sm.png)](https://lwn.net/Articles/1078543/)
细粒度锁也被添加了进来;“有些事情就是需要锁”。一个**新的垃圾收集器 (https://peps.python.org/pep-0703/#garbage-collection-cycle-collection)** 被加入,因为现有的垃圾收集器太复杂了。内存分配器被**切换到了 mimalloc (https://peps.python.org/pep-0703/#memory-management)**,这是一个线程安全的内存分配器,同时也提供了基于静止状态回收所需的钩子。
将所有这些东西结合在一起,就得到了无锁的列表、字典以及“其他重要类型”,他说道,这使得访问速度很快,尤其是对于拥有者线程;来自其他线程的访问仍然“相当快”。当执行诸如更改对象类型等操作时,需要停止所有线程以确保操作正确完成;“我们停止整个世界,然后进行更改,然后再次运行所有线程”。
还有提供**临界区 (https://peps.python.org/pep-0703/#python-critical-sections)** 的每对象锁。这些不是普通的锁,“它们是一种特殊的锁,不会死锁”。他指出,那些使用过锁的人可能会认为这个术语是自相矛盾的。但在 CPython 中确实如此,因为在临界区中重现了 GIL 的语义。现有的用于获取和释放 GIL 的调用被重新利用,以跟踪哪些线程没有在操作中阻塞,从而使得“停止整个世界”操作能够完成;将要阻塞的线程应该在阻塞之前就已经释放了 GIL。
最终结果是,“在自由线程 Python 中,线程语义*几乎*与有 GIL 的 Python 相同”。它避免了“可怕的内存模型、原子操作、内存顺序问题,你不需要内存屏障,这些都不重要,Python 就像 Python 一样运行”。
Wouters 指出,“自由线程”(free threaded)这个术语并非来自 Gross,而是来自指导委员会。它曾被称为“无 GIL”Python,但委员会不想最终让人们谈论“无无 GIL Python”。Wouters 说,“自由线程”并不是一个行业术语,它只是 Python 的一个术语,意思是 GIL 已被移除。这也有些用词不当,因为 Python 中的自由线程比 C 或 C++(可以在内存中随意写入而不受限制)或甚至 Rust(在这方面更好,但线程仍然比 Python 更自由)中的自由程度低得多。
#### 现状
Python 3.13 于 2024 年 10 月发布,包含了实验性的自由线程支持。它“相对较慢,单线程工作负载有 20-40% 的速度下降”。它存在线程安全问题以及一些可扩展性问题,增加更多线程可能会导致速度变慢。“但它证明了它可以工作。”
对于 3.14(于 2025 年 10 月发布),自由线程 Python “更加安全”且“更快”,单线程工作负载速度下降 0-10%。“我本人仍然很惊讶我们达到了 0% 的速度下降,那是在 Arm 硬件上,那里有一些魔法在起作用,我不知道是什么,但太神奇了。”在 Linux 上使用 GCC 时,通常约为 5%,尽管有时会达到 10%,特别是在使用旧编译器时。
他并不是批准 3.14 的 **PEP 779 (https://peps.python.org/pep-0779/)**(“自由线程 Python 支持状态的标准”)的指导委员会成员,但他是该 PEP 的共同作者。该 PEP 描述了需要做哪些工作才能将自由线程构建版本从“实验性”标签中移除,并使其成为该语言的一个受支持特性。**PEP 接受公告 (https://discuss.python.org/t/pep-779-criteria-for-supported-status-for-free-threaded-python/84319/123)** 移除了实验性标识,并描述了自由线程开发者未来需要做的工作。
今年 10 月即将发布的 Python 3.15 将为 GIL 版本和自由线程版本的解释器提供一个统一的、稳定的 ABI。这意味着扩展开发者可以一次性构建他们的代码,它将能够加载并在使用 GIL 或自由线程的 Python 3.15 或更高版本上运行。3.15 也将带来许多自由线程的可扩展性改进。
扩展开发者可能会被添加自由线程支持的前景所吓倒,但 Quansight 的开发者们已经整理了一份 **Python 自由线程指南 (https://py-free-threading.github.io/)**。其中包含关于**将扩展移植到自由线程解释器 (https://py-free-threading.github.io/porting-extensions/)** 的信息。“这并不难。”
C 全局变量需要被保护,因为 GIL 不再负责保护它们——如果它曾经真正保护过的话。“你也可以直接去掉你的 C 全局变量,这不是个坏主意。”可以使用临界区来保护 Python 对象中的可变数据。
相似文章
在自由线程Python中扩展NumPy
本文讨论了为改进NumPy在自由线程Python(无全局解释器锁的Python)上的性能所做的努力,从而实现了更好的并行性和可扩展性。
@DailyDoseOfDS_: 终于,Python 3.14 允许你禁用 GIL!这是个重大消息,因为之前即使你写了多线程代码,Pyth…
Python 3.14 引入了禁用全局解释器锁(GIL)的功能,实现了多线程代码的真正并行执行,克服了长期存在的限制。
Show HN: Runloom – 适用于Python自由线程的Go风格协程
Runloom是一个新的Python库,为自由线程Python(3.13t/3.14t)提供了Go风格的栈式协程和任务窃取调度器,使得阻塞代码能够在无GIL的情况下跨核心并发运行。它声称在生成吞吐量和调度性能上可以与Go匹敌或超越。
免费午餐已终结:软件领域向并发性的根本性转变 (2005)
文章讨论了由于物理限制,CPU速度提升带来的免费性能增益的终结,并强调随着多核处理器的兴起,软件开发向并发性的根本性转变。
2026年我不会用Python的项目
作者讨论了在Python聚会上的一次演讲,内容是关于2026年他们不再使用Python的项目,概述了其局限性和替代方案,同时也承认Python在某些领域仍然表现出色。