Python进阶:GIL(全局解释器锁)

一个不解之谜

  一段代码程序员

def CountDown(n):
    while n > 0:
        n -= 1

# CountDown(100000000)
#==8秒

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()
#==9s
  能够看出,多线程并无让上面代码变得更快,这是Python的问题?
  Python的线程,的的确确封装了底层操做系统线程。在Linux系统里是Pthread(全称为POSIXThread),而在Windows系统里是WindowsThread。

为何有GIL

  GIL,是最流行的 Python 解释器 CPython 中的一个技术术语。它的意思是全局解释器锁,本质上是相似操做系统的 Mutex。每个 Python 线程,在 CPython 解释器中执行时,都会先锁住本身的线程,阻止别的线程执行。CPython 会作一些小把戏,轮流执行 Python 线程。安全

  因此说,CPython 引进 GIL 其实主要就是这么两个缘由:
  一是设计者为了规避相似于内存管理这样的复杂的竞争风险问题(race condition);
  二是由于 CPython 大量使用 C 语言库,但大部分 C 语言库都不是原生线程安全的(线程安全会下降性能和增长复杂度)。

GIL 是如何工做的?

  如图,Thread 一、二、3 轮流执行,每个线程在开始执行时,都会锁住 GIL,以阻止别的线程执行;一样的,每个线程执行完一段后,会释放 GIL,以容许别的线程开始利用资源。
  
  
  CPython还有check interval机制。早期的 Python 是 100 个 ticks,大体对应了 1000 个 bytecodes;而 Python 3 之后,interval 是 15 毫秒。
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 的设计,主要是为了方便 CPython 解释器层面的编写者,而不是 Python 应用层面的程序员。多线程

  做为 Python 的使用者,咱们仍是须要 lock 等工具,来确保线程安全。好比下面的这个例子:工具

n = 0
lock = threading.Lock()

def foo():
    global n
    with lock:
        n += 1

如何绕过 GIL?

  事实上,不少高性能应用场景都已经有大量的 C 实现的 Python 库,例如 NumPy 的矩阵运算,就都是经过 C 来实现的,并不受 GIL 影响。 因此,大部分应用状况下,你并不须要过多考虑 GIL由于若是多线程计算成为性能瓶颈,每每已经有 Python 库来解决这个问题了。性能

  总的来讲,绕过 GIL 的大体思路有这么两种就够了:
  1. 绕过 CPython,使用 JPython(Java 实现的 Python 解释器)等别的实现;
  2. 把关键性能代码,放到别的语言(通常是 C++)中实现。

参考

  极客时间《Python核心技术实战》专栏ui