你见过Python的GIL吗

GIL是/Global Interpreter Lock/的简称,翻译为中文是/全局解释器锁/,维基百科的解释为:

全局解释器锁是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行。即便在多核心处理器上,使用 GIL 的解释器也只允许同一时间执行一个线程。

关于Python多线程与GIL的思考

问题的提出

学过Python的人大都知道这个解释性语言最通用的实现(CPython)采用了GIL的方式,因此在网上可以看到一些言论说“Python因为有GIL存在,多线程就算了,还是多进程吧”。
可这并不符合使用Python编程的实际体验,的确会让人产生一些疑惑。
Python有其自带的多线程模块,而且著名的爬虫框架scrapy可以同时爬多个网站,感觉上其并没有受到GIL的限制。
与Java对比的话,Java也支持多线程也可以写爬虫,而Java并没有GIL,这与Python看起来好像没有什么区别,那么GIL到底有没有发挥作用呢?

能否使用Java和Python分别写一段语义上一样的代码,通过两段程序的output有着明显的不同来证明GIL的确存在并且起了一定的作用呢?
要做这个事情首先要进行理论上的更进一步探索,才能进行代码的实现与output的设计。

关于并发的知识铺垫

<CSAPP>上提到了三种不同层面的 *并发编程技术*,分别为:

  1. 进程级别的并发;
  2. I/O多路复用;
  3. 线程级别的并发。

显然此篇的讨论应该归到第三种类型。

接下来,还要明确另一对容易搞错的概念, 并发并行
并发 指的是逻辑控制流在时间上的重叠,而 并行 则是指对多核CPU的利用。
并行只是并发的一个真子集,有种说法是“并发是基于逻辑上的同时发生,而并行是基于物理上的同时发生”。
所以,在只有一个CPU的机器上也可以运行并发程序,却不能运行并行程序。

使用加速比证明GIL存在的假设

根据以上关于并发与并行的基本知识,Python与Java在并发程序上的本质区别便可以得知。
即,因为有GIL的存在,Python无法利用到多核处理器的并行性,但依然可以编写除此之外的并发程序,并获得效率提升。而Java则无此限制。

CSAPP中提到了对于并行程序性能的衡量标准– 加速比

img

上述公式中,Sp称为加速比,其中p是处理器核的数量,Tp是指在p个核上程序的执行时间,当T1是程序顺序执行版本的执行时间时,Sp称为绝对加速比,而当Sp为程序并行版本在一个核上的执行时间时,Sp称为相对加速比。

所以,可以使用绝对加速比来证明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个线程下运行结果,measuretimecost 主要用来创建目标数量的线程,给各线程分配自己的计算任务,然后等待各线程全部返回,再加和,同时返回耗时,该函数实现为:

def measure_time_cost(thread_nums):
    nums = 99999999
    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版本的程序跑下来结果为:

img

而Java版本的相同实现,跑下来的结果为:

img

由于电脑核少,故主要看2核情况的对比,Python版本使用2核并没有得到明显的增速,加速比小于1。而Java版则差不多为2,发挥到了多核的效用,提高了计算密集性任务的效率。
随着线程数的增加,由于没有那么多核,线程切换的副作用体现了出来,后面时间会增加到比单线程还多。

之后,在知乎上有网友利用8核电脑做了验证,依然与预期相符,Java的最大加速比为0.701/0.168=4.17,而Python的加速比均小于0.5。

img

Java代码就是Executor提交任务,然后通过继承Callable利用Future得到结果。
完整版代码在这里,直接复制进code runner跑就可以看到结果,很方便。

这,可能是很多人第一次感受到GIL的存在吧~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351