python全局解释器锁GIL

568 阅读4分钟

业界的python程序员都会说python的线程是一个假的线程

python的线程,的的确确封装了底层的操作系统线程,在linux系统里是Pthread(全称为POSIX Thread),而在windows系统里是windows Thread。另外,python线程也完全受操作系统管理,比如协调何时执行,管理内存资源,管理中断等等。

所以,虽然python的线程和c++的线程本质上是不同的抽象,但它们的底层并没有不同。

为什么要有GIL

GIL是最流行的python解释器CPython中的一个技术术语。它的意思是全局解释器锁,本质上是类似操作系统的Mutex。每一个python线程,在CPython解释器中执行时,都会先锁住自己的线程,阻止别的线程执行。

当然,CPython会做一些小把戏,轮流执行Python线程。这样一来,用户看到的就是“伪并行”---python线程在交错执行,来模拟真正并行的线程。

为什么cpython需要GIL呢?这其实和cpython的实现有关。

cpython使用引用计数来管理内存,所以python脚本中创建的实例,都会有一个引用计数,来记录有多少个指针指向它。当引用计数只有0时,则会自动释放内存。

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3

这个例子中,a的引用计数是3,因为有a,b和作为参数传递的getrefcount这三个地方,都引用了一个空列表。

这样一来,如果有两个python线程同时引用了a,就会造成引用计数的race condition,引用计数可能最终只增加1,这样就会造成内存被污染。因为第一个线程结束时,会把引用计数减少1,这是可能达到条件释放内存,当第二个线程再试图访问a时,就找不到有效的内存了。

所以说,cpython引用GIL其实主要就是这么两个原因:

1.是设计者为了规避类似于内存管理这样的复杂的竞争风险问题(race condition);

2.是因为cpython大量使用c语言库,但大部分c语言库都不是原生线程安全的(线程安全会降低性能和增加复杂度)

GIL是如何工作的

下面这张图,就是一个GIL在python程序的工作示例。其中,Thread1, 2, 3轮流执行,每一个线程在开始执行时,都会锁住GIL,以阻住别的线程执行;同样的,每一个线程执行完一段后,会释放GIL,以允许别的线程开始利用资源。

为什么python线程会去主动释放GIL呢?

毕竟,如果仅仅是要求python线程在开始执行时锁住GIL,而永远不去释放GIL,那别的线程就都没有了运行的机会。

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

不同版本的 Python 中,check interval 的实现方式并不一样。早期的 Python 是 100 个 ticks,大致对应了 1000 个 bytecodes;而 Python 3 以后,interval 是 15 毫秒。当然,我们不必细究具体多久会强制释放 GIL,这不应该成为我们程序设计的依赖条件,我们只需明白,CPython 解释器会在一个“合理”的时间范围内释放 GIL 就可以了。

整体来说,每一个 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。

GIL与多线程的关系

GIL 只支持单线程,而python支持多线程,这两者之间究竟是什么关系呢?

其实,GIL的存在与pytohn支持多线程并不矛盾。GIL指的是同一时刻,程序只能有一个线程运行;而python中的多线程,是指多个线程交替执行,造成一个“伪并行”的结果,但是具体到某一时刻,仍然只有1个线程在运行,并不是真正的多线程并行。这个机制,可以看下面来表示:

例如说,我用10个线程来爬取50个网站的内容。线程1在爬取第一个网站时,被I/O block住了,处于等待状态;这时,GIL就会释放,而线程2就会开始执行,去爬取第2个网站,依次类推。等到线程1的I/O操作完成时,主程序便又会切回线程1,让其完成剩下的操作,这样一来,从用户的角度看到的,便是我们所说的多线程。