深入浅出Python多线程(0)GIL全局解释器锁的前世今生

请输入图片描述
图片来源于网络

谈到Python多线程机制,总会说到GIL全局解释器锁,说“臭名昭著”有点过分,但不可否认,Python程序员一向对此诟病比较多,人们担心GIL会影响到多线程程序的性能。

我们先来看这样一个程序,这是一个CPU计算密集型的程序。

import time

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

if __name__ == '__main__':
    c = 100000000
    start_time = time.time()
    countdown(c)
    print('cost {}s'.format(time.time() - start_time))
#output
>>> cost 2.11941504478s

我再使用多线程来跑一个这个程序:

import time
import threading

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

if __name__ == '__main__':
    c = 100000000
    t1 = threading.Thread(target=countdown, args=(c // 2,))
    t2 = threading.Thread(target=countdown, args=(c // 2,))
    start_time = time.time()
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('cost {}s'.format(time.time() - start_time))
#output
>>> cost 3.90731191635s

可以看到,顺序执行的情况下耗时2.11秒,而在使用多线程处理的时候,我开启了两个线程,把计算量一分为二,分别交给两个线程去执行,同样的计算量,结果是3.90秒。在使用多线程的情况下反而慢了差不多2倍。多线程执行不是应该效率更高的吗?到底发生了什么?其实这一切都是Python GIL惹的祸。

深入理解Python GIL,对于我们理解多线程编程,写出高效的程序至关重要!

注:以上结果是在Python2.7下运行得出的,在Python3下,由于GIL机制的改变,顺序执行和多线程执行的结果差距不大。即使如此,我们也很明显看到多线程并没有带来的效率提升。

什么是GIL(全局解释器锁)

GIL全称是Global Interpreter Lock,他仅仅允许一个线程持有Python解释器的控制权,这意味着在同时一刻只有一个线程是处于执行状态的。这就表示Python的多线程程序并不能利用多核CPU的优势。

这么傻的东西为什么还会存在呢?这还得从Python的内存管理机制说起。

Python的内存管理机制采用对象引用计数,对象自身维护一个引用计数变量来记录对自身的引用。对象的创建、引用、参数调用、存入容器等等都会使引用计数器+1,反之,当使用del,重新赋值、离开作用域、容器销毁等等操作都会使得引用计数器-1,当一个对象的引用计数器为0时,将释放对象占用的内存。

>>> import sys
>>> a = 'helloworld'   # 对象创建,引用计数器 +1
>>> b = a              # 被引用,引用计数器 +1
>>> l = [a]            # 放入容器,引用计数器 +1
>>> sys.getrefcount(a) # 让我们使用getrefcount()看看被引用了几次
4                      # a作为参数传给getrefcount的时候又+1
>>> l.remove(a)        # 从容器删除,引用计数器 -1
>>> del b              # 显式销毁,引用计数器 -1
>>> a = 'world'        # 重新赋值,引用计数器 -1

那么问题来了,当两个线程共享这个引用计数变量时,由于其是非线程安全的,那么两个线程可能同时增加或修改引用计数器的值,可能会导致内存泄漏,甚至在引用还存在的情况下,内存已经被释放,这就麻烦了。

GIL设计本意正是为了解决这个问题的,在解释器上挂上一把锁,任何一个线程执行的时候都需要获取锁,操作完再释放,以此来保证线程安全。而且由于只有一把锁,并不会带来很大的性能开销。

当如下情况发生时,GIL会释放。

  1. I/O密集型程序,在I/O操作之前GIL总是被释放,允许其他线程在等待这个I/O的时候执行。
  2. 指定数量的字节码指令(100个)。
  3. 固定时间15ms线程主动让出控制。

GIL无疑是一个历史遗留问题,放在当时的环境来看,GIL的实现非常容易,非线程安全的C库也很容易集成。

Python3为什么不删除GIL

GIL这么讨厌为什么还不删除?Guido van van Rossum在这里曾经表达了删除GIL并不容易

I'd welcome it if someone did another experiment along the lines of Greg's patch (which I haven't found online), and I'd welcome a set of patches into Py3k only ifthe performance for a single-threaded program (and for a multi-threaded but I/O-bound program) does not decrease.

只有在单线程程序(以及I/O密集型的多线程程序)性能不降低的情况下,我才欢迎在Py3K中安装一组补丁程序。

与GIL单线程的性能优势相比,删除GIL会使得Python3比Python2更慢。

有何影响?

首先,GIL只会影响到那些计算密集型程序,比如严重依赖CPU计算的。而对于我们I/O密集型的,比如网络、数据库、文件访问,使用多线程程序是很有帮助的,因为I/O操作大部分的时间是等待。

CPU-bound类的程序优化方案

  • 对于计算效率低下的程序,可以考虑C语言扩展模块
  • 如果要操作数组,那么使用NumPy
  • 还可以使用PyPy来优化执行效率
  • 创建一个进程池,充分利用多核资源,每个进程会启动一个单独的Python解释器来工作。

版权声明

© 著作权归作者所有
允许自由转载,但请保持署名和原文链接。 不允许商业用途、盈利行为及衍生盈利行为。

Understanding the Python GIL
UnderstandingGIL.pdf

标签: python gil, 全局解释器锁, global interpreter lock, python多线程

添加新评论