GIL是/Global Interpreter Lock/的简称,翻译为中文是/全局解释器锁/,维基百科的解释为:git
全局解释器锁是计算机程序设计语言解释器用于同步线程的一种机制,它使得任什么时候刻仅有一个线程在执行。即使在多核心处理器上,使用 GIL 的解释器也只容许同一时间执行一个线程。github
学过Python的人大都知道这个解释性语言最通用的实现(CPython)采用了GIL的方式,所以在网上能够看到一些言论说“Python由于有GIL存在,多线程就算了,仍是多进程吧”。
可这并不符合使用Python编程的实际体验,的确会让人产生一些疑惑。
Python有其自带的多线程模块,并且著名的爬虫框架scrapy能够同时爬多个网站,感受上其并无受到GIL的限制。
与Java对比的话,Java也支持多线程也能够写爬虫,而Java并无GIL,这与Python看起来好像没有什么区别,那么GIL到底有没有发挥做用呢?编程
可否使用Java和Python分别写一段语义上同样的代码,经过两段程序的output有着明显的不一样来证实GIL的确存在而且起了必定的做用呢?
要作这个事情首先要进行理论上的更进一步探索,才能进行代码的实现与output的设计。多线程
<CSAPP>上提到了三种不一样层面的 *并发编程技术*,分别为:并发
显然此篇的讨论应该归到第三种类型。框架
接下来,还要明确另外一对容易搞错的概念, 并发 与 并行 。
并发 指的是逻辑控制流在时间上的重叠,而 并行 则是指对多核CPU的利用。
并行只是并发的一个真子集,有种说法是“并发是基于逻辑上的同时发生,而并行是基于物理上的同时发生”。
因此,在只有一个CPU的机器上也能够运行并发程序,却不能运行并行程序。scrapy
根据以上关于并发与并行的基本知识,Python与Java在并发程序上的本质区别即可以得知。
即,由于有GIL的存在,Python没法利用到多核处理器的并行性,但依然能够编写除此以外的并发程序,并得到效率提高。而Java则无此限制。函数
CSAPP中提到了对于并行程序性能的衡量标准– 加速比 。 性能
因此,可使用绝对加速比来证实GIL的存在。 预期是,写一段无IO的计算密集性任务,分别交给Python与Java的一个(顺序执行)、多个线程(并行版本)去运行,算出各自的加速比,若是Python版本加速比小于1,而Java版本的加速比在计算机核心数左右,则说明是GIL起了做用,致使Python程序没法发挥多核的并行性。网站
依然使用书中的例子: 作一个加法任务,从0加到0x7fffffff求和,经过设置线程数n,将数字加和任务平均拆分为n份,给到各线程作本身的一份,最后将子任务的和再加和求得最后的结果。
那么当n等于1时,即为顺序版本,n大于1时则为并行版本。
书中代码使用C语言实现,此处分别改写为Python与Java两个版本。
入口为:
def main():
thread_num1 = 1
thread_num2 = 2
thread_num4 = 4
thread_num8 = 8
print ("sum_task with thread_num1 cost time: " + str(measure_time_cost(thread_num1)) + "s in Python version.")
print ("sum_task with thread_num2 cost time: " + str(measure_time_cost(thread_num2)) + "s in Python version.")
print ("sum_task with thread_num4 cost time: " + str(measure_time_cost(thread_num4)) + "s in Python version.")
print ("sum_task with thread_num8 cost time: " + str(measure_time_cost(thread_num4)) + "s in Python version.")
复制代码
分别用尝试1,2,4,8个线程下运行结果,measure_time_cost
主要用来建立目标数量的线程,给各线程分配本身的计算任务,而后等待各线程所有返回,再加和,同时返回耗时,该函数实现为:
def measure_time_cost(thread_nums):
nums = 99999999 # Python加到0x7fffffff要过久,改一个小一点的值。
num_per_thread = int((nums + 1) / thread_nums)
thread_list = [None] * thread_nums
task_list = [None] * thread_nums
start_at = time.time()
for i in range(thread_nums):
ct = SumTask()
thread_list[i] = threading.Thread(target=ct.run, args=(i, num_per_thread))
thread_list[i].start()
task_list[i] = ct
for i in range(thread_nums):
thread_list[i].join()
end_at = time.time()
result = 0
for i in range(thread_nums):
result += task_list[i].get_result()
print (result)
return end_at - start_at
复制代码
用到的SumTask就是一个简单的类用来处理返回值,不想去用queue,全局变量什么的。
因为笔者的mac只有两核,没法看到4核、8核等更明显的效果,Python版本的程序跑下来结果为:
而Java版本的相同实现,跑下来的结果为:
因为电脑核少,故主要看2核状况的对比,Python版本使用2核并无获得明显的增速,加速比小于1。而Java版则差很少为2,发挥到了多核的效用,提升了计算密集性任务的效率。
随着线程数的增长,因为没有那么多核,线程切换的反作用体现了出来,后面时间会增长到比单线程还多。
以后,在知乎上有网友利用8核电脑作了验证,依然与预期相符,Java的最大加速比为0.701/0.168=4.17,而Python的加速比均小于0.5。
Java代码就是Executor提交任务,而后经过继承Callable利用Future获得结果。 完整版代码在这里,直接复制进code runner跑就能够看到结果,很方便。
这,多是不少人第一次感觉到GIL的存在吧~