VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > Python基础教程 >
  • Python3基础之Python并发之GIL的限制(重点)

GIL是什么

首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。

就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。

所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL

那么CPython实现中的GIL又是什么呢?GIL全称Global Interpreter Lock为了避免误导,我们还是来看一下官方给出的解释:

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

从解释中,我们大致可以得出结论,GIL是一个防止解释器多线程并发执行机器码的一个全局互斥锁。其存在主要是因为在代码执行过程中,CPython的内存管理不是线程安全的。

为什么会有GIL

由于物理上得限制,各CPU厂商在核心频率上的比赛已经被多核所取代。为了更有效的利用多核处理器的性能,就出现了多线程的编程方式,而随之带来的就是线程间数据一致性和状态同步的困难。即使在CPU内部的Cache也不例外,为了有效解决多份缓存之间的数据同步时各厂商花费了不少心思,也不可避免的带来了一定的性能损失。

Python当然也逃不开,为了利用多核,Python开始支持多线程。而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。 于是有了GIL这把超级大锁,而当越来越多的代码库开发者接受了这种设定后,他们开始大量依赖这种特性(即默认python内部对象是thread-safe的,无需在实现时考虑额外的内存锁和同步操作)。

慢慢的这种实现方式被发现是蛋疼且低效的。但当大家试图去拆分和去除GIL的时候,发现大量库代码开发者已经重度依赖GIL而非常难以去除了。有多难?做个类比,像MySQL这样的“小项目”为了把Buffer Pool Mutex这把大锁拆分成各个小锁也花了从5.5到5.6再到5.7多个大版为期近5年的时间,并且仍在继续。MySQL这个背后有公司支持且有固定开发团队的产品走的如此艰难,那又更何况Python这样核心开发和代码贡献者高度社区化的团队呢?

所以简单的说GIL的存在更多的是历史原因。如果推到重来,多线程的问题依然还是要面对,但是至少会比目前GIL这种方式会更优雅。

GIL的影响

从上文的介绍和官方的定义来看,GIL无疑就是一把全局排他锁。毫无疑问全局锁的存在会对多线程的效率有不小影响。甚至就几乎等于Python是个单线程的程序。 那么读者就会说了,全局锁只要释放的勤快效率也不会差啊。只要在进行耗时的IO操作的时候,能释放GIL,这样也还是可以提升运行效率的嘛。或者说再差也不会比单线程的效率差吧。理论上是这样,而实际上呢?Python比你想的更糟。

下面我们就对比下Python在多线程和单线程下得效率对比。测试方法很简单,一个循环1亿次的计数器函数。一个通过单线程执行两次,一个多线程执行。最后比较执行总时间。测试环境为双核的Mac pro。注:为了减少线程库本身性能损耗对测试结果带来的影响,这里单线程的代码同样使用了线程。只是顺序的执行两次,模拟单线程。

顺序执行的单线程(single_thread.py)


  1.  
    #! /usr/bin/python
  2.  
     
  3.  
    from threading import Thread
  4.  
    import time
  5.  
     
  6.  
    def my_counter():
  7.  
    i = 0
  8.  
    for _ in range(100000000):
  9.  
    i = i + 1
  10.  
    return True
  11.  
     
  12.  
    def main():
  13.  
    thread_array = {}
  14.  
    start_time = time.time()
  15.  
    for tid in range(2):
  16.  
    t = Thread(target=my_counter)
  17.  
    t.start()
  18.  
    t.join()
  19.  
    end_time = time.time()
  20.  
    print("Total time: {}".format(end_time - start_time))
  21.  
     
  22.  
    if __name__ == '__main__':
  23.  
    main()
  •  

同时执行的两个并发线程(multi_thread.py)


  1.  
    #! /usr/bin/python
  2.  
     
  3.  
    from threading import Thread
  4.  
    import time
  5.  
     
  6.  
    def my_counter():
  7.  
    i = 0
  8.  
    for _ in range(100000000):
  9.  
    i = i + 1
  10.  
    return True
  11.  
     
  12.  
    def main():
  13.  
    thread_array = {}
  14.  
    start_time = time.time()
  15.  
    for tid in range(2):
  16.  
    t = Thread(target=my_counter)
  17.  
    t.start()
  18.  
    thread_array[tid] = t
  19.  
    for i in range(2):