# Python GIL - CPython
# 前言
看一个例子,可以看到,执行相同的程序多线程反而比单线程更慢。
def CountDown(n):
while n > 0:
n -= 1
n = 100000000
CountDown(n)
# time 5.4s
from threading import Thread
n = 100000000
t1 = Thread(target=CountDown, args=[n // 2])
t2 = Thread(target=CountDown, args=[n // 2])
t1.start()
t2.start()
t1.join()
t2.join()
# time 9.6s
# 由于GIL的存在只能跑满一个CPU核心,没有利用到多核
# 甚至由于线程切换的代价使得速度还不如原先
# GIL概述
GIL,是最流行的 Python 解释器 CPython 中的一个技术术语。它的意思是全局解释器锁,本质上是类似操作系统的 Mutex。每一个 Python 线程,在 CPython 解释器中执行时,都会先锁住自己的线程,阻止别的线程执行。
- GIL存在于解释器级别,意思即为如果自己实现一个Python解释器,完全可以不使用GIL。
- 任何线程执行前必须先获得GIL锁,然后每执行100条字节码(指Python2,Python3中为15毫秒),解释器就自动释放GIL锁,让别的线程有机会执行。
GIL(全局解释器锁)是一种机制,它限制了同一时刻只有一个线程能够执行 Python 代码。这意味着即使电脑有多个处理器核心,也无法同时执行多个 Python 线程。
这种限制有助于避免多个线程同时访问共享数据时产生的问题,但它也会限制 Python 程序的并行性。
甚至于即便有了GIL,Python线程也不一定安全。
CPython 会做一些小把戏,轮流执行 Python 线程。这样一来,用户看到的就是“伪并行”——Python 线程在交错执行,来模拟真正并行的线程。
CPython 中还有另一个机制,叫做 check_interval,意思是 CPython 解释器会去轮询检查线程 GIL 的锁住情况。每隔一段时间,Python 解释器就会强制当前线程去释放 GIL,这样别的线程才能有执行的机会。
因此即使是多线程,但同一时间仍只有一个线程在执行-是“伪并行”。
CPython引进GIL的主要原因是:
- 设计者为了规避类似内存管理这样的复杂竞争风险问题(race condition);
- CPython大量使用C语言库,但大部分C语言库都不是线程安全的(线程安全会降低性能和增加复杂度)。
- GIL 的设计,主要是为了方便 CPython 解释器层面的编写者,而不是 Python 应用层面的程序员。
对于IO密集型任务,Python的多线程能起到作用;但对于CPU密集型任务,Python多线程几乎占不到优势,甚至有可能因为争夺资源而变慢。
- 在单核 CPU 环境下,多线程在执行时,线程 A 释放了 GIL 锁,那么被唤醒的线程 B 能够立即拿到 GIL 锁,线程 B 可以无缝接力继续执行
- 而在多核CPU环境下,多线程执行时,线程 A 在 CPU0 执行完之后释放 GIL 锁,其他 CPU 上的线程都会进行竞争。但 CPU0 上的线程 B 可能又马上获取到了 GIL,这就导致其他 CPU 上被唤醒的线程,只能眼巴巴地看着 CPU0 上的线程愉快地执行着,而自己只能等待,直到又被切换到待调度的状态,这就会产生多核 CPU 频繁进行线程切换,消耗资源,这种情况也被叫做「CPU颠簸」。
- 因此如果使用多线程运行一个CPU密集型任务,那么Python多线程是无法提高运行效率的。
# GIL工作原理-GIL和check_interval
下面这张图,就是一个 GIL 在 Python 程序的工作示例。其中,Thread 1、2、3 轮流执行,每一个线程在开始执行时,都会锁住 GIL,以阻止别的线程执行;同样的,每一个线程执行完一段后,会释放 GIL,以允许别的线程开始利用资源。

CPython 中还有另一个机制,叫做 check_interval,意思是 CPython 解释器会去轮询检查线程 GIL 的锁住情况。每隔一段时间,Python 解释器就会强制当前线程去释放 GIL,这样别的线程才能有执行的机会。
关于这个时间,早期的 Python 是 100 个 ticks,大致对应了 1000 个 bytecodes;而 Python 3 以后,interval 是 15 毫秒。
整体来说,每一个 Python 线程都是类似这样循环的封装,我们来看下面这段代码:
for (;;) {
if (--ticker < 0) {
ticker = check_interval;
/* Give another thread a chance */
PyThread_release_lock(interpreter_lock);
/* Other threads may run now */
PyThread_acquire_lock(interpreter_lock, 1);
}
bytecode = *next_instr++;
switch (bytecode) {
/* execute the next instruction ... */
}
}
从这段代码中,我们可以看到,每个 Python 线程都会先检查 ticker 计数。只有在 ticker 大于 0 的情况下,线程才会去执行自己的 bytecode。
# Python的线程安全
及时GIL同时只允许徐一个Python线程执行,但是Python还有check interval的抢占机制。考虑如下代码:
import threading
n = 0
def foo():
global n
n += 1
threads = []
for i in range(100):
t = threading.Thread(target=foo)
threads.append(t)
for t in threads:
t.start()
for t in threads:
t.join()
print(n)
如果执行多次的话,能够发现会出现打印99或98的情况,因为n+=1这句代码让线程并不安全,我们看一下foo函数的bytecode:
>>> import dis
>>> dis.dis(foo)
LOAD_GLOBAL 0 (n)
LOAD_CONST 1 (1)
INPLACE_ADD
STORE_GLOBAL 0 (n)
#TODO#:
这段在3.7.16和3.10下多次运行结果都是正确的,但是从dis的情况来看 n+=1 似乎不是原子操作,因此是会发生race condition的,结果不应该一直是正确的。
这就表明,即使有了GIL我们仍然不能完全保证线程安全,仍需要时刻注意。
但与此同时,我们可以通过lock等工具,来保证线程安全。
n = 0
lock = threading.Lock()
def foo():
global n
with lock:
n += 1
# 绕过GIL
- GIL是CPython的限制,只要使用 JPython(Java 实现的 Python 解释器)等别的实现即可不再受GIL限制。
- 很多高性能应用场景已经由C实现的Python库,他们并不通过CPython来解释执行,也就不再受GIL的限制。
- 但如果应用真的对性能有超级严格的要求,比如 100us 就对你的应用有很大影响,那我必须要说,Python 可能不是你的最优选择。
- 可以把关键性能(performance-critical)代码在 C++ 中实现(不再受 GIL 所限),然后再提供 Python 的调用接口。
# GIL与Python多线程
- GIL 只支持单线程,而 Python 支持多线程
- GIL 的存在与 Python 支持多线程并不矛盾。前面我们讲过,GIL 是指同一时刻,程序只能有一个线程运行;而 Python 中的多线程,是指多个线程交替执行,造成一个“伪并行”的结果,但是具体到某一时刻,仍然只有 1 个线程在运行,并不是真正的多线程并行。
← 进程间通信 Python并发编程概述 →