Cython和GIL
原文: https://cython.readthedocs.io/en/latest/src/userguide/nogil.html
Python有一个全局锁(全局解释器锁,GIL)来保证Python解释器相关的数据没有被污染。在Cython中当你没有访问Python数据的有些时候释放GIL可能会很有用。
有两种场景你可能会想释放GIL。
1. 使用Cython的并行化机制。 prange
代码块的内容要求必须是 nogil
的。
2. 你希望其他外部的Python线程能够同时运行。
2.1. 如果你有一个比较大的不需要GIL的计算/IO-密集的代码块,释放GIL可能会帮助到希望用多线程来使用这个库的用户。但是这只是有用,但不是必须的。
2.2. (非常、非常少见的情况)在长期运行的Cython代码中,你的代码从来不需要调用Python解释器,那么你可以用简短的 with nogil: pass
代码块来总体上释放GIL。这是因为Cython并不会立刻释放(和Python解释器不同),所以如果你在等待另一个Python线程来完成某个任务,这么做可以避免死锁。除非你在用Cython编译在界面的代码,否则第二点应该对你不太适用。
如果这两点都和你无关,那你可能并不需要释放GIL。那种可以在没有GIL下工作的代码(不调用Python,纯粹的C级别的数值运算)也通常是运行性能最高的代码。这有时会让人产生相反的错觉、并且认为是因为释放GIL所以才快而不是因为代码本身。不要误会了。你的(单线程)代码有、或者没有GIL都跑的一样快。
将函数标记为可以在没有GIL时运行
你可以通过在函数签名末尾追加 nogil
或者使用 @cython.nogil
装饰器装饰函数的方式把一个函数(一个Cython函数或者是外部函数)标记为 nogil
。
# 经典风格
cdef void some_func() noexcept nogil:
....
# 纯Python风格
@cython.nogil
@cython.cfunc
@cython.noexcept
def some_func() -> None:
...
请记住这并不意味着在调用该函数的时候GIL会被(自动)释放。它仅仅意味着该函数很适合在释放GIL的情况下调用。即使在持有GIL的情况下,也可以调用这个函数。
在这种场景下,我们把函数标记为 noexcept
来代表它不会引发任何Python异常。请记住:一个标记了 except *
异常声明的函数(通常是返回 void
的函数)调用成本是很高的因为Cython在每次调用之后暂时获取GIL以检查异常状态。大多数其他异常在一个 nogil
代码块中处理的成本都是比较低的,因为GIL仅在异常真的被抛出的时候才会需要被获取。
释放(并重新获取)GIl
为了真正释放GIL,你可以使用上下文管理器。
# 经典风格
with nogil:
... # 在没有GIL下运行的代码
with gil:
... 需要GIL运行的代码
... # 更多在没有GIL下运行的代码
# 纯Python风格
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
来保证GIL会在调用之后立刻获取。这目前仅在纯Cython(经典风格)下支持(纯Python风格下没有对应的语义存在)。
cdef int some_func() with gil:
...
有条件的获取GIL
可以在编译的时候根据一定条件来决定是否释放GIL。这在和融合类型一起使用的时候最有用。(译者注:因为融合类型可以代表不同的类型,有些可以释放GIL,有些不可以)
# 经典风格
with nogil(some_type is not object):
... # some code that runs without the GIL, unless we're processing objects
# 纯Python风格
with cython.nogil(some_type is not object):
... # some code that runs without the GIL, unless we're processing objects
异常和GIL
少量的Python操作可以在 nogil
代码块中执行、且不需要显式的声明 with gil
。主要的例子是抛出异常的时候。这里Cython理解一个异常总是需要GIL,因此会隐式的获取GIL。类似的,当一个 nogil
函数抛出异常的时候,Cython可以正确的传播它而你不必显式的编写代码来处理它。在大多数情况下这会显得很高效因为Cython可以使用函数的异常声明来检查异常,然后当必须的时候自动获取GIL,但是 except *
仍然显得很低效因为这意味着Cython 总是要 重新获取GIL。
别把GIL当成锁一样用
你也许很想把GIL当成自己的锁一样使用然后可以说"整个 with gil
代码块会因为我们使用了GIL而具有原子性"。千万别这么做。
GIL主要是给解释器用的,而不是你。这里有两个问题:
. 未来版本的Python解释器有可能会破坏你的”锁“
. 第二点,GIL可能会在任何Python代码执行完之后释放。最简单的办法是毁掉一个带有 __del__
函数的Python对象。还有很多其他有创意的办法可以实现,并且你几乎不可能知道你什么时候会遇到这些办法中的其中一种情况。
如果你想要一把可靠的锁你应该使用标准库的 threading
模块提供的工具。