Cython 和 GIL¶
Python 拥有一个全局锁 (GIL) 以确保与 Python 解释器相关的数据不会被破坏。在您不访问 Python 数据时,有时在 Cython 中释放此锁会很有用。
在两种情况下,您可能希望释放 GIL
使用 Cython 的并行机制。例如,
prange
循环的内容必须为nogil
。如果您希望其他(外部)Python 线程能够同时运行。
如果您有一个大型计算密集型/IO 密集型代码块,它不需要 GIL,那么释放它可能是一种“礼貌”的做法,只是为了让希望进行多线程的用户受益。但是,这主要是有用的,而不是必要的。
(非常非常偶尔地)在长时间运行的 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:
...
cdef void some_func() noexcept nogil:
....
请注意,这不会在调用函数时释放 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 nogil:
... # some code that runs without the GIL
with 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
cdef int some_func() with gil:
...
with 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
with 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
模块中的工具。