Python GIL(全局解释器锁)
一 前言
''' 定义: 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.) ''' 结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
首先需要明确的一点是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
关于GIL对python多线程的影响可参考:http://www.dabeaz.com/python/UnderstandingGIL.pdf
二 GIL介绍
GIL(Global Interpreter Lock,全局解释器锁)本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。
可以肯定的一点是:保护不同的数据的安全,就应该加不同的锁。
要想了解GIL,首先确定一点:每次执行python程序,都会产生一个独立的进程。例如:python test1.py,python test2.py,python test3.py会产生3个不同的python进程
''' #验证python test.py只会产生一个进程 #test1.py内容 import os,time print(os.getpid()) time.sleep(100) print('end') ''' python3 test1.py #在windows下 tasklist |findstr python #在linux下 ps aux |grep python
在一个python的进程内,不仅有test1.py的主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内。
在这个进程内,有如下特点:
- 所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码)。例如:test.py定义一个函数work(代码内容如下图),在进程内所有线程都能访问到work的代码,于是我们可以开启三个线程然后target都指向该代码,能访问到意味着就是可以执行。
- 所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。
所以,如果多个线程的target=work,那么执行流程是:多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行
解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题:对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,如下图的GIL,保证python解释器同一时间只能执行一个任务的代码
综上,GIL是Python虚拟机(CPython)用来互斥线程对Python虚拟机的使用。具备了GIL,在一个线程拥有了解释器的访问权之后,其他所有线程都必须等待它释放解释器的访问权,即使这些线程的下一条指令并不会互相影响。因此,在调用任何CPython API之前,要先获得GIL
GIL缺点:多处理器退化为单处理器;优点:避免大量的加锁解锁操作
以python官方Cpython解释器为代表….其他语言好像未见。
作为解释型语言,Python的解释器必须做到既安全又高效。我们都知道多线程编程会遇到的问题。解释器要留意的是避免在不同的线程操作内部共享的数据。同时它还要保证在管理用户线程时总是有最大化的计算资源。那么,不同线程同时访问时,数据的保护机制是怎样的呢?答案是解释器全局锁GIL。GIL对诸如当前线程状态和为垃圾回收而用的堆分配对象这样的东西的访问提供着保护。
首先,在早期的python解释器依赖较多的全局状态,传承下来,使得想要移除当今的GIL变得更加困难。其次,对于程序员而言,仅仅是想要理解它的实现就需要对操作系统设计、多线程编程、C语言、解释器设计和CPython解释器的实现有着非常彻底的理解。 在1999年,针对Python1.5,一个“freethreading”补丁已经尝试移除GIL,用细粒度的锁来代替。然而,GIL的移除给单线程程序的执行速度带来了一定的负面影响。当用单线程执行时,速度大约降低了40%。虽然使用两个线程时在速度上得到了提高,但这个提高并没有随着核数的增加而线性增长。因此这个补丁没有被采纳。 另外,在python的不同解释器实现中,如PyPy就移除了GIL,其执行速度更快(不单单是去除GIL的原因)。然而,我们通常使用的CPython占有着统治地位的使用量,所以,你懂的。 在Python 3.2中实现了一个新的GIL,并且带着一些积极的结果。这是自1992年以来,GIL的一次最主要改变。旧的GIL通过对Python指令进行计数来确定何时放弃GIL。在新的GIL实现中,用一个固定的超时时间来指示当前的线程以放弃这个锁。在当前线程保持这个锁,且当第二个线程请求这个锁的时候,当前线程就会在5ms后被强制释放掉这个锁(这就是说,当前线程每5ms就要检查其是否需要释放这个锁)。当任务是可行的时候,这会使得线程间的切换更加可预测。
最大的影响是我们不能随意使用多线程。要区分任务场景。 在单核cpu情况下对性能的影响可以忽略不计,多线程多进程都差不多。在多核CPU时,多线程效率较低。GIL对单进程和多进程没有影响。
建议在IO密集型任务中使用多线程,在计算密集型任务中使用多进程。深入研究python的协程机制。
更多的详细介绍和说明请参考下面的文献:
原文:https://jeffknupp.com/blog/2012/03/31/pythons-hardest-problem/
译文:http://www.oschina.net/translate/pythons-hardest-problem
三 GIL VS Lock
疑问:Python中已经有一个GIL来保证同一时间只能有一个线程执行,为什么还需要Lock?
首先,我们需要知道:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据
其次,我们需要知道:保护不同的数据就应该加不同的锁
最后,我们就会了解:GIL与Lock 是两把锁,保护的数据不同,GIL保护的是解释器级的数据(比如:垃圾回收的),保护用户自己的数据则需要自己加锁处理,如下图(需要说明的是:Python中的线程是操作系统的原生线程):

from threading import Thread,Lock import time def work(): global n lock.acquire() temp = n time.sleep(0.1) n = temp - 1 lock.release() if __name__ == '__main__': lock = Lock() n = 100 t_l = [] for i in range(100): t = Thread(target=work) t_l.append(t) t.start() for t in t_l: t.join() print(n) # 结果肯定为0,由原来的并发执行变成了串行,牺牲了执行效率但是保证了数据安全,不加锁则结果可能为99
过程分析:所有线程抢的是GIL锁,或者说所有线程抢的是执行权限
线程1抢到GIL锁,拿到执行权限,开始执行,然后加了一把Lock,还没有执行完毕,即线程1还未释放Lock,有可能线程2抢到GIL锁,开始执行,执行过程中发现Lock还没有被线程1释放,于是线程2进入阻塞,被夺走执行权限,有可能线程1拿到GIL,然后正常执行到释放Lock。这就导致了串行运行的效果
既然是串行,那我们执行
t1.start() t1.join t2.start() t2.join()
这也是串行执行啊,为何还要加Lock呢,需知join是等待t1所有的代码执行完,相当于锁住了t1的所有代码,而Lock只是锁住一部分操作共享数据的代码(可参考上一篇文件:PYTHON并发编程之多进程)。
因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序里的线程和py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题,这可以说是Python早期版本的遗留问题。
1. 有100个线程去抢GIL锁,即抢执行权限 2. 肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire() 3. 极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL 4. 直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程
四 GIL与多线程
有了GIL的存在,无论我们启多少个线程,计算机有多少个cpu,Python在执行一个进程的时候,在同一时刻只允许一个线程运行,python是无法利用多核CPU实现多线程的。

所以,这里就存在一个问题:进程可以利用多核,但是开销大;python的多线程开销小,但却无法利用多核优势。这点很坑啊!!!
那我们如何解决这个问题呢?首先,我们需要明确几点:
- cpu到底是用来做计算的,还是用来做I/O的?
- 多cpu,意味着可以有多个核并行完成计算,所以多核提升的是计算性能
- 每个cpu一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处
所以,对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用。当然对运行一个程序来说,随着cpu的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地,举例如下:
现在有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是: 方案一:开启四个进程 方案二:一个进程下,开启四个线程 # 单核情况下,分析结果: 如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜 如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜 # 多核情况下,分析结果: 如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行,用不上多核,方案一胜 如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜 # 结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换), # 但是,对于IO密集型的任务效率还是有显著提升的。
五 多线程性能测试
from multiprocessing import Process from threading import Thread import os,time def work(): res = 0 for i in range(50000000): res *= i if __name__ == '__main__': l = [] print(os.cpu_count()) # 本机为4核 start = time.time() for i in range(4): p = Process(target=work) # 耗时7.854 # p = Thread(target=work) # 耗时15.957 l.append(p) p.start() for p in l: p.join() stop = time.time() print('run time is %s' %(stop-start))
from multiprocessing import Process from threading import Thread import os,time def work(): time.sleep(2) print('=====') if __name__ == '__main__': l = [] print(os.cpu_count()) # 本机为4核 start = time.time() for i in range(100): # p = Process(target=work) # 耗时8.356,大部分时间耗费在创建进程上 p = Thread(target=work) # 耗时2.035 l.append(p) p.start() for p in l: p.join() stop = time.time() print('run time is %s' %(stop-start))
实际应用:
- 多线程用于IO密集型,如:socket,爬虫,web
- 多进程用于计算密集型,如金融分析


浙公网安备 33010602011771号