关于我
编程界的一名小程序猿,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是咱们团队的主要技术栈。 联系:hylinux1024@gmail.comhtml
A global interpreter lock (GIL) is a mechanism used in computer-language interpreters to synchronize the execution of threads so that only one native thread can execute at a time. --引用自wikipediapython
从上面的定义能够看出,GIL
是计算机语言解析器用于同步线程执行的一种同步锁机制。不少编程语言都有GIL
,例如Python
、Ruby
。linux
Python
做为一种面向对象的动态类型编程语言,开发者编写的代码是经过解析器顺序解析执行的。 大多数人目前使用的Python
解析器是CPython
提供的,而CPython
的解析器是使用引用计数来进行内存管理,为了对多线程安全的支持,引用了global intepreter lock
,只有获取到GIL
的线程才能执行。若是没有这个锁,在多线程编码中即便是简单的操做也会引发共享变量被多个线程同时修改的问题。例若有两个线程同时对同一个对象进行引用时,这两个线程都会将变量的引用计数从0增长为1,明显这是不正确的。
能够经过sys
模块获取一个变量的引用计数git
>>> import sys
>>> a = []
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
复制代码
sys.getrefcount()
方法中的参数对a的引用也会引发计数的增长。github
是否能够对每一个变量都分别使用锁来同步呢?数据库
若是有多个锁的话,线程同步时就容易出现死锁,并且编程的复杂度也会上升。当全局只有一个锁时,全部线程都在竞争一把锁,就不会出现相互等待对方锁的状况,编码的实现也更简单。此外只有一把锁时对单线程的影响其实并非很大。编程
Python
核心开发团队以及Python
社区的技术专家对移除GIL
也作过屡次尝试,然而最后都没有令各方满意的方案。小程序
内存管理技术除了引用计数外,一些编程语言为了不引用全局解析锁,内存管理就使用垃圾回收机制。安全
固然这也意味着这些使用垃圾回收机制的语言就必须提高其它方面的性能(例如JIT
编译),来弥补单线程程序的执行性能的损失。
对于Python
的来讲,选择了引用计数做为内存管理。一方面保证了单线程程序执行的性能,另外一方面GIL
使得编码也更容易实现。
在Python
中不少特性是经过C
库来实现的,而在C
库中要保证线程安全的话也是依赖于GIL
。bash
因此当有人成功移除了GIL
以后,Python
的程序并无变得更快,由于大多数人使用的都是单线程场景。
首先来GIL
对IO
密集型程序和CPU
密集型程序的的区别。 像文件读写、网络请求、数据库访问等操做都是IO
密集型的,它们的特色须要等待IO
操做的时间,而后才进行下一步操做;而像数学计算、图片处理、矩阵运算等操做则是CPU
密集型的,它们的特色是须要大量CPU
算力来支持。
对于IO
密集型操做,当前拥有锁的线程会先释放锁,而后执行IO
操做,最后再获取锁。线程在释放锁时会把当前线程状态存在一个全局变量PThreadState
的数据结构中,当线程获取到锁以后恢复以前的线程状态
用文字描述执行流程
保存当前线程的状态到一个全局变量中
释放GIL
... 执行IO操做 ...
获取GIL
从全局变量中恢复以前的线程状态
复制代码
下面这段代码是测试单线程执行500万次消耗的时间
import time
COUNT = 50000000
def countdown(n):
while n > 0:
n -= 1
start = time.time()
countdown(COUNT)
end = time.time()
print('Time taken in seconds -', end - start)
# 执行结果
# Time taken in seconds - 2.44541597366333
复制代码
在个人8核的macbook
上跑大约是2.4秒,而后再看一个多线程版本
import time
from threading import Thread
COUNT = 50000000
def countdown(n):
while n > 0:
n -= 1
t1 = Thread(target=countdown, args=(COUNT // 2,))
t2 = Thread(target=countdown, args=(COUNT // 2,))
start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()
print('Time taken in seconds -', end - start)
# 执行结果
# Time taken in seconds - 2.4634649753570557
复制代码
上文代码每一个线程都执行250万次,若是线程是并发的,执行时间应该是上面单线程版本的一半时间左右,然而在我电脑中执行时间大约为2.5秒! 多线程不但没有更高效率,反而还更耗时了。这个例子就说明Python
中的线程是顺序执行的,只有获取到锁的线程能够获取解析器的执行时间。多线程执行多出来的那点时间就是获取锁和释放锁消耗的时间。
那如何实现高并发呢?
答案是使用多进程。前面的文章有介绍多进程的使用
from multiprocessing import Pool
import time
COUNT = 50000000
def countdown(n):
while n > 0:
n -= 1
if __name__ == '__main__':
pool = Pool(processes=2)
start = time.time()
r1 = pool.apply_async(countdown, [COUNT // 2])
r2 = pool.apply_async(countdown, [COUNT // 2])
pool.close()
pool.join()
end = time.time()
print('Time taken in seconds -', end - start)
# 执行结果
# Time taken in seconds - 1.2389559745788574
复制代码
使用多进程,每一个进程运行250万次,大约消耗1.2秒的时间。差很少是上面线程版本的一半时间。
固然还可使用其它Python
解析器,例如Jython
、IronPython
或PyPy
。
既然每一个线程执行前都要获取锁,那么有一个线程获取到锁一直占用不释放,怎么办?
IO
密集型的程序会主动释放锁,但对于CPU
密集型的程序或IO
密集型和CPU
混合的程序,解析器将会如何工做呢?
早期的作法是Python
会执行100条指令后就强制线程释放GIL
让其它线程有可执行的机会。
能够经过如下获取到这个配置
>>> import sys
>>> sys.getcheckinterval()
100
复制代码
在个人电脑中还打印了下面的输出警告
Warning (from warnings module):
File "__main__", line 1
DeprecationWarning: sys.getcheckinterval() and sys.setcheckinterval() are deprecated. Use sys.getswitchinterval() instead.
复制代码
意思是sys.getcheckinterval()
方法已经废弃,应该使用sys.getswitchinterval()
方法。 由于传统的实现中每解析100指令的就强制线程释放锁的作法,会致使CPU
密集型的线程会一直占用GIL
而IO
密集型的线程会一直得不到解析的问题。因而新的线程切换方案就被提出来了
>>> sys.getswitchinterval()
0.005
复制代码
这个方法返回0.05秒,意思是每一个线程执行0.05秒后就释放GIL
,用于线程的切换。
在CPython
解析器的实现因为global interpreter lock
(全局解释锁)的存在,任什么时候刻都只有一个线程能执行Python
的bytecode
(字节码)。
常见的内存管理方案有引用计数和垃圾回收,Python
选择了前者,这保证了单线程的执行效率,同时对编码实现也更加简单。想要移除GIL
是不容易的,即便成功将GIL
去除,对Python
的来讲是牺牲了单线程的执行效率。
Python
中GIL
对IO
密集型程序能够较好的支持多线程并发,然而对CPU
密集型程序来讲就要使用多进程或使用其它不使用GIL
的解析器。
目前最新的解析器实现中线程每执行0.05秒就会强制释放GIL
,进行线程的切换。