一直以来,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()`,只要不在线程之间共享它们,它们会更安全。