CPython的全局解释器锁-GIL即将成为历史

一直以来,Python 线程在执行 CPU 密集型操作时,都无法实现真正的并行性,因为线程之间会相互让出 CPU。Python 3.13 引入了无 GIL(全局解释器锁)的构建方式,这是自 CPython 解释器创建以来最大的架构性变化之一。Python 线程终于可以并行运行,以全速执行。

无 GIL 的 Python 直到最近还只是实验性功能,但随着 Python 3.14 beta 3 的发布,它已正式被支持。无 GIL 的构建方式仍然是可选的,可以选择尝鲜。

以下是一些入门建议:

  • 先问自己 Python 线程能为你做什么

Python 线程长期以来并不适合真正的并行性,因此直到现在,你可能从未考虑过它作为实现并行性的可能性。现在,随着无 GIL 的 Python 正式被支持,可以考虑它的使用场景了。

任何语言中的线程都采用"分而治之"的方法来处理任务。任何"明显适合并行"的任务都适合使用线程。但必须指出,并非所有问题都能在多个线程中均匀分割;有些问题在使用线程时可能会变得复杂,出现问题时,难以调试。

举个例子,如果你有一个任务需要写入大量文件,如果每个任务都单独运行在一个线程中,效率会较低,因为写入文件本身是一个串行 操作。更好的方法是将任务分配到多个线程中,并使用一个线程专门负责写入磁盘。当每个任务完成后,它将工作发送给磁盘写入任务。这样,任务之间不会互相阻塞,也不会因文件写入而被阻塞。

  • 使用最高层次的线程抽象

Python 中有多个层次的线程抽象:

你可以直接创建线程并使用 threading.Thread 来管理它们。这种情况下,每个线程的生命周期需要自己管理,并且还要管理线程完成后的等待和结果获取。这在程序中其他操作需要等待线程完成时是可行的,对于更高级的任务就不太理想。

concurrent.futures.ThreadPoolExecutor 是一个更高层次的抽象。它创建一个线程池,可以设置线程数量,以响应传入的请求。你可以将任务提交给线程池,并在需要时获取结果,这样程序就不会因等待任务完成而被阻塞。

这两种方法都是对底层 _thread 模块的抽象,而 _thread 模块本身则是对操作系统级线程处理的抽象。你使用的抽象层次越高,无 GIL 的 Python 行为越可能符合预期。

一般来说,任何在 Python 中使用的线程(而不是在外部,如扩展模块中使用的线程)都应该在 Python 中创建。理论上,你可以在 CPython 扩展中创建线程并将其注册到解释器中,但这样做并没有太大意义,因为解释器已经很好地完成了这些工作。

如果你已经在使用 ProcessPoolExecutor 作为抽象,那么在无 GIL 的构建中,你可以轻松地将其替换为 ThreadPoolExecutor。两者的接口相同,因此只需修改导入语句即可。

  • 确保 Cython 模块是线程安全的

无 GIL 的 Python 最大的障碍之一是确保用 C(或具有 C 兼容接口的语言)编写的 CPython 扩展能够尊重新的无 GIL 设计。

Cython 是 Python 中编写 C 扩展的关键工具,它紧密跟踪 CPython 运行时的变化。最近,Cython 的维护者添加了对无 GIL 构建的支持,但你仍然需要确保你的代码是线程安全的。具体来说,当与 Python 对象交互时,代码必须是线程安全的。

如果你已经确信你的代码是线程安全的,你可以在 Cython 模块中添加一个指令,并在无 GIL 构建中测试它:

python 复制代码
# cython: freethreading_compatible = True

这将标记该模块与无 GIL 构建兼容。如果你在无 GIL 构建中导入一个未标记的模块,解释器会自动重新启用 GIL 以确保安全。

为了增加现有 Cython 模块的线程安全性,你可以使用 Cython 添加的几个工具来简化工作:

关键代码段(Critical sections):这是一个上下文管理器,它接受一个 Python 对象,并在上下文块的持续时间内为其创建一个 CPython 关键代码段或本地锁。它也可以作为函数装饰器使用,通常用于类方法(锁应用于类实例)。关键代码段会自动防止死锁,但代价是不能保证锁在整个关键代码段中持续持有------如果其他对象需要锁,锁可能会被释放并重新获取。

**PyMutex 锁:**这些是更强大的锁,需要显式获取和释放。请注意,如果你在非无 GIL 构建(如为了向后兼容)中使用它们,那么在 PyMutex 锁期间重新获取 GIL 会带来死锁的风险。

  • 不要在不同线程之间共享迭代器或帧对象

一些对象不应该在不同线程之间共享,因为它们的内部状态不是线程安全的。两个常见的例子是迭代器和帧对象。

Python 中的迭代器对象基于某些内部状态生成一个对象流。例如,生成器是创建迭代器对象的常见方式。

如果你创建了一个迭代器,不要尝试在不同线程之间传递它。你可以共享迭代器生成的对象(只要它们是线程安全的),但不要在不同线程之间共享迭代器本身。例如,要创建一个迭代器,按顺序生成字符串中的字母,你可以这样做:

python 复制代码
data = "abcdefg"
d_iter = iter(data)
item = next(d_iter)
item2 = next(d_iter)
# ... 等等

在这个例子中,`d_iter` 是迭代器对象。你可以在线程之间共享 `data` 和 `item`(或 `item2` 等),但不能在线程之间共享 `d_iter` 本身,因为这可能会破坏其内部状态。

Python 的帧对象包含程序在执行过程中的某个特定点的状态信息。它们被 Python 的调试机制用于在出现错误条件时提供有关程序的详细信息。

帧对象也不是线程安全的。如果你在 Python 程序中通过 `sys.current_frames()` 访问帧对象,那么在无 GIL 的构建中你可能会遇到问题。然而,如果你使用 `inspect.currentframe()` 或 `sys._getframe()`,只要不在线程之间共享它们,它们会更安全。

相关推荐
alicema111116 分钟前
萤石摄像头C++SDK应用实例
开发语言·前端·c++·qt·opencv
爱代码的小黄人17 分钟前
MATLAB中绘制系统零极点图(Pole-Zero Map)的几种方法
开发语言·matlab
伍哥的传说22 分钟前
Vue3 Anime.js超级炫酷的网页动画库详解
开发语言·前端·javascript·vue.js·vue·ecmascript·vue3
The Chosen One98523 分钟前
C++ :vector的介绍和使用
开发语言·c++
一只爱做笔记的码农24 分钟前
【C#】Vscode中C#工程如何引用自编写的dll
开发语言·vscode·c#
楼田莉子24 分钟前
Linux学习之认识Linux的基本指令
linux·运维·服务器·开发语言·学习
ahauedu43 分钟前
jar命令提取 JAR 文件
java·jar
疾跑哥布林升级版1 小时前
网络编程7.17
开发语言·网络
青岛少儿编程-王老师1 小时前
CCF编程能力等级认证GESP—C++1级—20250628
java·开发语言·c++
几道之旅1 小时前
Electron实现“仅首次运行时创建SQLite数据库”
数据库·electron·sqlite