GIL 机制

官方 Python 使用的是 CPython 解释器,然而 CPython 解释器存在一个弊端,即同一时间只有一个线程在执行。这个原因是 Python 设计之初的一个缺陷,即全局解释器锁 GIL。

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

GIL 的目的是为了避免多个线程同时执行 Python 字节码,因为 CPython 的内存管理机制并不是线程安全的。但是进而也降低了多线程下的执行效率。

而 GIL 的引入首先需要了解 CPython 的内存管理机制。CPython 使用引用计数法来进行内存管理,如果两个线程同时对一个对象进行操作,那么可能会出现内存泄漏。为了避免数据不一致的情况,就引入了锁的机制。为了避免一个对象一个锁进而导致死锁的可能性,就创建了 GIL 锁。但也就意味着 CPU 密集型程序的效率会退化成单线程执行。

GIL 锁的底层原理:每一个线程在开始执行时,都会锁住 GIL,以阻止别的线程执行;同样的,每一个线程执行完一段后,会释放 GIL,以允许别的线程开始利用资源。

每一个线程在开始执行时,都会锁住 GIL,以阻止别的线程执行;同样的,每一个线程执行完一段后,会释放 GIL,以允许别的线程开始利用资源。

CPython 中还有另一个机制,叫做 check_interval,意思是 CPython 解释器会去轮询检查线程 GIL 的锁住情况。每隔一段时间,Python 解释器就会强制当前线程去释放 GIL,这样别的线程才能有执行的机会。

不同版本的 Python 中,check interval 的实现方式并不一样。早期的 Python 是 100 个 ticks,大致对应了 1000 个 bytecodes;而 Python 3 以后,interval 是 15 毫秒。

为了避免同一线程霸占 CPU,在 python3.2 版本之后,线程会自动的调整自己的优先级,使得多线程任务执行效率更高。

但就算使用 GIL,也并不能保证完全的线程安全,例如 n = n + 1 操作是分为 4 步来执行的,并不是一个原子操作。具体来说为:加载全局变量 n、加载常数 1、进行二进制加法运算、将运算结果存入变量 n。线程在执行 4 步的时候可能会让出 GIL 锁,从而导致出错。

Link:zhuanlan.zhihu.com/p/97218985

image.png|600

绕过 GIL 锁

Python 的 GIL,是通过 CPython 的解释器加的限制。如果你的代码并不需要 CPython 解释器来执行,就不再受 GIL 的限制。

事实上,很多高性能应用场景都已经有大量的 C 实现的 Python 库,例如 NumPy 的矩阵运算,就都是通过 C 来实现的,并不受 GIL 影响。

可以理解的是,我们难以避免的有时候就是想临时给自己松松绑,摆脱 GIL,比如在深度学习应用里,大部分代码就都是 Python 的。在实际工作中,如果我们想实现一个自定义的微分算子,或者是一个特定硬件的加速器,那我们就不得不把这些关键性能(performance-critical)代码在 C++ 中实现(不再受 GIL 所限),然后再提供 Python 的调用接口。

参考链接

23 | 你真的懂Python GIL(全局解释器锁)吗?-Python核心技术与实战-极客时间
zhuanlan.zhihu.com/p/97218985