Cython 和 GIL

Python 拥有一个全局锁 (GIL) 以确保与 Python 解释器相关的数据不会被破坏。在您不访问 Python 数据时,有时在 Cython 中释放此锁会很有用。

在两种情况下,您可能希望释放 GIL

  1. 使用 Cython 的并行机制。例如,prange 循环的内容必须为 nogil

  2. 如果您希望其他(外部)Python 线程能够同时运行。

    1. 如果您有一个大型计算密集型/IO 密集型代码块,它不需要 GIL,那么释放它可能是一种“礼貌”的做法,只是为了让希望进行多线程的用户受益。但是,这主要是有用的,而不是必要的。

    2. (非常非常偶尔地)在长时间运行的 Cython 代码中,该代码从不调用 Python 解释器,有时使用一个简短的 with nogil: pass 块来短暂释放 GIL 可能很有用。这是因为 Cython 不会自发地释放它(与 Python 解释器不同),因此如果您正在等待另一个 Python 线程完成任务,这可以避免死锁。此子要点可能不适用于您,除非您使用 Cython 编译 GUI 代码。

如果这两个要点都不适用,那么您可能不需要释放 GIL。可以无需 GIL 运行的 Cython 代码类型(不调用 Python,纯粹的 C 级数字运算)通常是高效运行的代码类型。这有时会让人产生一种印象,即反过来也是如此,诀窍在于释放 GIL,而不是他们正在运行的实际代码。不要被这种印象误导——您的(单线程)代码无论是否使用 GIL 都会以相同的速度运行。

将函数标记为能够在没有 GIL 的情况下运行

您可以通过将此附加到函数签名或使用 @cython.nogil 装饰器来将整个函数(Cython 函数或 外部函数)标记为 nogil

@cython.nogil
@cython.cfunc
@cython.noexcept
def some_func() -> None:
...

请注意,这不会在调用函数时释放 GIL。它只是表明函数适合在释放 GIL 时使用。在持有 GIL 时调用这些函数也是可以的。

在这种情况下,我们已将函数标记为 noexcept,以表明它不能引发 Python 异常。请注意,具有 except * 异常规范的函数(通常是返回 void 的函数)调用起来会很昂贵,因为 Cython 需要在每次调用后暂时重新获取 GIL 以检查异常状态。大多数其他异常规范在 nogil 块中处理起来很便宜,因为只有在实际抛出异常时才会获取 GIL。

释放(并重新获取)GIL

要实际释放 GIL,您可以使用上下文管理器

with cython.nogil:
    ...              # some code that runs without the GIL
    with cython.gil:
        ...          # some code that runs with the GIL
    ...              # some more code without the GIL

with gil 块是一个有用的技巧,允许在非 GIL 块中执行一小段 Python 代码或 Python 对象处理。尽量不要过度使用它,因为等待和获取 GIL 会产生成本,而且因为这些块不能并行运行,因为所有执行都需要相同的锁。

可以使用 with gil 标记函数,或使用 @cython.with_gil 装饰器,以确保在调用函数时立即获取 GIL。

@cython.with_gil
@cython.cfunc
def some_func() -> cython.int
    ...

with cython.nogil:
    ...          # some code that runs without the GIL
    some_func()  # some_func() will internally acquire the GIL
    ...          # some code that runs without the GIL
some_func()      # GIL is already held hence the function does not need to acquire the GIL

有条件地获取 GIL

可以根据编译时条件释放 GIL。这在使用 融合类型(模板) 时最常使用。

with cython.nogil(some_type is not object):
    ...  # some code that runs without the GIL, unless we're processing objects

异常和 GIL

nogil 块中,无需显式使用 with gil 即可执行少量“Python 操作”。主要示例是抛出异常。Cython 知道异常始终需要 GIL,因此会隐式重新获取 GIL。类似地,如果 nogil 函数抛出异常,Cython 能够正确地传播异常,而无需编写显式代码来处理它。在大多数情况下,这很有效率,因为 Cython 能够使用函数的异常规范来检查错误,然后仅在需要时获取 GIL,但 except * 函数效率较低,因为 Cython 必须始终重新获取 GIL。

不要将 GIL 用作锁

您可能很想尝试将 GIL 用于自己的锁定目的,并说“整个 with gil 块将原子地运行,因为我们拥有 GIL”。不要这样做!

GIL 仅供解释器使用,不供您使用。这里有两个问题

#. 未来对 Python 解释器的改进可能会破坏您的“锁定”。

#. 其次,如果执行任何 Python 代码,则可能会释放 GIL。运行任意 Python 代码的最简单方法是销毁具有 __del__ 函数的 Python 对象,但还有许多其他创造性的方法可以做到这一点,几乎不可能知道您不会触发其中之一。

如果您需要可靠的锁,请使用标准库中 threading 模块中的工具。