Levy's ink.
Doodles, whimsy & life.
About
Blog
Mess
Catalog

Python并发编程和GIL

如果尝试用Nodejs解决过问题的都会知道,Node中是不存在线程的概念的:所有代码都是跑在一个线程上,用户代码不会被中断,只有I/O或一些库函数调用时会交出CPU占有权。相比之下,Python就有线程,看上去更能利用多核机器。事实是否如此呢?我们先跑下面一段程序:(程序内容转载自Dave Beazley在PyCon 2010的展示)

代码1

def countDown(n):
    while n>0:
        n-=1

COUNT=50000000
stime=time.time()
countDown(COUNT)
etime=time.time()

print etime-stime

代码2

def countDown(n):
    while n>0:
        n-=1

COUNT=50000000
t1=Thread(target=countDown,args=(COUNT//2,))
t2=Thread(target=countDown,args=(COUNT//2,))
stime=time.time()
t1.start()
t2.start()
t1.join()
t2.join()
etime=time.time()

print etime-stime

代码目的很简单,不再赘述,代码2是代码1的多线程优化版。如果对线程工作原理有最基础了解的人就知道在多核机器上,代码2的运行时间会比代码1少,理想时甚至接近一半。那么在我的笔记本上的程序运行结果如何呢?

  • CPU:Intel Core i7-4500U,1.8GHz*4
  • Python version: 2.7.8
  • 代码1运行时间:1.84s
  • 代码2运行时间:7.42s

再次声明,以上实验代码非本人原创,预知来源请科学上网至https://www.youtube.com/watch?v=Obt-vMVdM8s查看展示视频,这也是本文的重要信息来源。

我没有看错吧,或者你写反了?

没有。如果不相信大可以本机reproduce。

GIL(Global Interpreter Lock)

为了解释上文令人匪夷所思的现象,GIL或称全局解释锁,是不得不提的东西。这货在相当多文章中被以“infamous”(臭名昭著)形容,可见其在众程序员心目中的印象。这么说并不是排斥单线程或类似行为,而是它坑坏了一大波冲着python多线程对CPU的完全利用性而为之完整设计系统并实现了绝大部分的程序员(我也是其中苦逼的一只)。

简而言之,GIL是在CPython(Python官方提供的解释器)中用到的一把互斥锁,尽管python的线程模型的确是OS中真真切切的thread,但任何时候只能有一个线程拥有该锁,故实际执行的线程在任何时候只有一个。

更精细地说,GIL会在程序进行IO和一些纯函数性计算时释放该锁。但对于上文中代码这样并没有IO的native code,其串行本质便暴露无遗。仔细看前文的人可能会发现,这和Node在计算力上有什么区别呢!?!?——我在意识到GIL的存在的时候心中便是如此咆哮的...

到此,文章应该结束了。但依然有问题没有解释:即使是多线程单执行,其充其量莫过于在单核机上跑多线程,带来的额外开销也就线程间切换的syscall,不至于造成4倍时间差吧?接下来我们将着重讨论这个问题,为了解答它,首先必须将python线程切换过程更深入的剖析一下:

为防止某一线程占用大量CPU致其他线程饥饿(Starvation),python解释器会每解释指定数目的指令后强行释放GIL并试图马上拿回,同时所有其他线程苏醒,对该锁进行竞争,从而给予其他线程一定执行机会。看上去和OS的进程/线程调度方式并没有太大区别——是的,但当python和os同时在两个层面对线程进行调度时,问题出现了,由于os并不能为python维护的GIL建立优先队列,故在多核环境下,空闲CPU将在每一个释放周期控制一个线程抢夺GIL,失败后继续睡眠,苏醒,再睡眠。同时,该释放周期设计得相当短,导致在当条件允许时其他没有拿到GIL的线程会进行海量的系统调用,同时伴随着高频线程切换,从而带来大量时间开销。前文提到的演讲作者Dave在其个人主页上用可视化图标展示了这一可怕的过程:http://www.dabeaz.com/GIL/gilvis/index.html

Python 3.2中,CPython采用了新的GIL机制,据称能很大程度减轻由于CPU竞争带来的额外开销,尽量让“多线程和单线程一样快”。说来讽刺,如此流行的编程语言竟在线程处理上落得此结果...伴随着我这个Python新手一起入坑吧。

所以说,该怎么进行并发编程?

多进程,这是标准答案。Python其实给了与threading非常接近的multiprocessing,其中提供了对应的互斥锁等同步机制,不得不说实现得非常精致和用户友好。可不管再精致,资源不互通、需要靠管道和共享内存维持进程间通信的硬伤仍然给线程间交流频繁的并发模型实现带来很多麻烦。

当然,多进程还是多线程一直也是程序员争论不休的热点,论证其好与不好在当今就和争论什么语言最好一样没意义。

所以,要用真正的多线程请求助于C/C++/Java/C#。

对于脚本语言,借用少爷的话,不要谈性能,谈谈信仰。

可惜我的信仰是Node...至少人家单线程得光明磊落,还省下了写程序时为保持线程和数据同步所消耗的脑力。

参考文献