GIL" />

Python 中的 GIL

人们只瞧见了上帝关了门,却没瞅到上帝也开了窗

什么是GIL?

GIL全局解释器锁(英语:Global Interpreter Lock,缩写GIL),是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行。即便在多核心处理器上,使用 GIL 的解释器也只允许同一时间执行一个线程。

From wikipedia

为什么需要 GIL 呢?网上很多博客都说是历史遗留问题。但实际上,我认为这和 Python 的内存管理机制有关。在 Python 中,每个对象都维护着一个引用计数,而当这个计数变为 0 时,这个对象将会被回收。如果没有 GIL,两个进程对同一个对象的引用计数的更改就会导致错误,这里举个例子:

进程 X 删除对象 A 使得其引用计数减 1 变为 0,对象 A 被回收。进程 Y 删除对象 A,这本报错的。但由于没有 GIL,这两个进程同时进行,所以没有报错。这是错误的。

因此,GIL 的重要性不言而喻。但也是因为它,使得 Python 的多线程活生生地由并行变成了并发。

为了减少 GIL 所带来的性能损耗,我们能做什么呢?

关于Python内存管理机制,你可以查看这篇文章: https://www.cnblogs.com/geaozhang/p/7111961.html#yinyongjishu 

被GIL削弱的多线程

由于全局解释锁(GIL)的原因,Python 的线程被限制到同一时刻只允许一个线程执行这样一个执行模型。所以,Python 的线程更适用于处理I/O和其他需要并发执行的阻塞操作(比如等待I/O、等待从数据库获取数据等等),而不是需要多处理器并行的计算密集型任务。

From Python cookbook

下面是一例实验:

import queue
import time
import threading

q = queue.Queue()


def init_queue():
    for i in range(10):
        q.put(i)
        print('队列初始化完成')


def job():
    while not q.empty():
        time.sleep(0.5)
        data = q.get()
        time.sleep(0.5)
        print('任务完成')


if __name__ == '__main__':
    init_queue()
    print('=====单线程十次作业开始=====')
    start_time = time.time()
    for _ in range(10):
        job()
        print(f'作业时间:{time.time() - start_time}')
    print('=====单线程十次作业完成=====')

    init_queue()
    print('=====多线程十次作业开始=====')
    start_time = time.time()
    thread_list = [threading.Thread(target=job) for _ in range(10)]
    for t in thread_list:
        t.start()
        for t in thread_list:
            if t.is_alive():
                t.join()
    print(f'作业时间:{time.time() - start_time}')
    print('=====多线程十次作业完成=====')
队列初始化完成
=====单线程十次作业开始=====
任务完成
任务完成
任务完成
任务完成
任务完成
任务完成
任务完成
任务完成
任务完成
任务完成
作业时间:10.010543823242188
=====单线程十次作业完成=====
队列初始化完成
=====多线程十次作业开始=====
任务完成
任务完成
任务完成
任务完成
任务完成
任务完成
任务完成
任务完成
任务完成
任务完成
作业时间:1.0054144859313965
=====多线程十次作业完成=====

值得注意的是,如果将 job 函数中后面一个 sleep 函数去掉,会导致多线程测试不能完成。具体原因还未弄清。

那么,计算密集型任务呢?

对于 IO 密集型任务,Python 的伪多线程可以解决,但是对于计算密集型任务,它仍旧无法真正在同一时间调用多个函数。这个时候,多线程的作用就出来了。

import multiprocessing
import queue
import time

q = queue.Queue()


def init_queue():
    for i in range(10):
        q.put(i)
        print('队列初始化完成')


def job():
    while not q.empty():
        time.sleep(0.5)
        data = q.get()
        for i in range(20):
            data *= i
            time.sleep(0.5)
            print('任务完成')


if __name__ == '__main__':
    init_queue()
    print('=====单线程十次作业开始=====')
    start_time = time.time()
    for _ in range(10):
        job()
        print(f'作业时间:{time.time() - start_time}')
    print('=====单线程十次作业完成=====')

    init_queue()
    print('=====多进程十次作业开始=====')
    start_time = time.time()
    process_list = [multiprocessing.Process(target=job) for _ in range(10)]
    for t in process_list:
        t.start()
        for t in process_list:
            if t.is_alive():
                t.join()
    print(f'作业时间:{time.time() - start_time}')
    print('=====多进程十次作业完成=====')

输出结果:

队列初始化完成

=====单线程十次作业开始=====

任务完成

任务完成

任务完成

任务完成

任务完成

任务完成

任务完成

任务完成

任务完成

任务完成

作业时间:10.008376598358154

=====单线程十次作业完成=====

队列初始化完成

=====多进程十次作业开始=====

任务完成

任务完成

任务完成

任务完成

任务完成

任务完成

任务完成

任务完成

任务完成

任务完成

作业时间:1.4181747436523438

=====多进程十次作业完成=====

虽然GIL给Python的性能关上了一扇门,但是这并不意味着我们就要忽略标准库里为我们打开的每一扇窗。