Python的GIL与多线程、多进程

GIL是什么,Python中什么时候用多线程,什么时候用多进程?
本篇博文主要是摘录汇总对以上问题的解答。


GIL是什么

GIL 的全称为 Global Interpreter Lock ,意即全局解释器锁。在 Python 语言的主流实现 CPython 中,GIL 是一个货真价实的全局线程锁,在解释器解释执行任何 Python 代码时,都需要先获得这把锁才行,在遇到 I/O 操作时会释放这把锁。如果是纯计算的程序,没有 I/O 操作,解释器会每隔 100 次操作就释放这把锁,让别的线程有机会执行(这个次数可以通过 sys.setcheckinterval 来调整)。所以虽然 CPython 的线程库直接封装操作系统的原生线程,但 CPython 进程做为一个整体,同一时间只会有一个获得了 GIL 的线程在跑,其它的线程都处于等待状态等着 GIL 的释放。

也就是说:
对于任何 Python 程序,不论有多少线程,多少处理器,任何时候都只有一个线程在执行。

为什么GIL如此设计

GIL 直接导致 CPython 不能利用物理多核的性能加速运算。那为什么会有这样的设计呢?我猜想应该还是历史遗留问题。多核 CPU 在 1990 年代还属于类科幻,Guido van Rossum 在创造 python 的时候,也想不到他的语言有一天会被用到很可能 1000+ 个核的 CPU 上面,一个全局锁搞定多线程安全在那个时代应该是最简单经济的设计了。简单而又能满足需求,那就是合适的设计(对设计来说,应该只有合适与否,而没有好与不好)。怪只怪硬件的发展实在太快了,摩尔定律给软件业的红利这么快就要到头了。短短 20 年不到,代码工人就不能指望仅仅靠升级 CPU 就能让老软件跑的更快了。在多核时代,编程的免费午餐没有了。如果程序不能用并发挤干每个核的运算性能,那就意谓着会被淘汰。

多线程与多进程

想要利用多核系统,Python 必须支持多线程。作为解释型语言,Python 的解释器对多线程的支持必须是既安全又高效的。我们都知道多线程编程带来的问题。解释器必须避免不同的线程操作内部共享的数据。同时还要保证用户线程能完成尽量多的计算。

那么在不同线程同时访问数据时,怎样才能保护数据呢?答案是全局解释器锁。顾名思义,这是一个加在解释器上的全局锁(从互斥量或者类似意义上来看)。这种方式是很安全,但是(对于 Python 初学者来说)这也就意味着:对于任何 Python 程序,不论有多少线程,多少处理器,任何时候都只有一个线程在执行。

许多人都是偶然发现这个事实。网上的讨论组和留言板充斥着来自 Python 初学者和专家提出的类似的问题:为什么我全新的多线程 Python 程序运行得比其只有一个线程的时候还要慢?在问这个问题时,许多人还觉得自己像个傻瓜,因为如果程序确实是可并行的,那么两个线程的程序显然要比单线程要快。事实上,问及这个问题的次数实在太多了,Python 的专家们已经为它准备了一个标准答案:不要使用多线程,请使用多进程。但这个答案比问题本身更加让人困惑:难道我不能在 Python 中使用多线程?在 Python 这样流行的语言中使用多线程究竟是有多糟糕,连专家都建议不要使用。是我哪里没有搞明白吗?

很遗憾,并不是。由于 Python 解释器的设计,使用多线程以提高性能可以算是一个困难的任务。在最坏的情况下,多线程反而会降低(有时很明显)程序的运行速度。一个计算机科学专业的新生就可以告诉你:当多个线程竞争一个共享资源时将会发生什么。结果通常不理想。很多情况下多线程都能很好地工作,对于解释器的实现和内核开发人员来说,不要对 Python 多线程性能有太多抱怨可能是他们最大的心愿。

适用场景讨论

如果python代码是CPU密集型多个线程的代码很有可能是线性执行的。所以这种情况下多线程是鸡肋,效率可能还不如单线程因为有上下文切换。
如果python代码是IO密集型,多线程可以明显提高效率。例如网络爬虫,绝大多数时间爬虫是在等待socket返回数据。遇到 I/O 操作时会释放全局锁,正在执行的线程在等待IO的时候,其他线程可以继续执行,效率得以提高。

对于IO密集型的程序,Python多线程还是有很大作用的。然而 Python 3 引入的 asyncio 模块使得很多 IO 操作有了更好的方式去解决,这就非常类似 Node.js 了,都是没有多线程,而是采用 Event Loop 来处理耗时的 IO 操作。

一般大部分的观点是由于有GIL的存在,Python中的多线程不能真正的利用多核,不能解决 cpu bound 的问题,但是在一些 IO bound 的程序上却可以有很好的提升。但是目前的情况是 我们有了协程啊,在 2.x 系列里我们可以使用 gevent 啊,在 3.x 系列的标准库里又有了 asyncio 。IO bound 的问题完全可以用协程解决。而且我们可以自主的控制协程的调度了。为什么还要使用由 OS 调度的不太可控的线程呢?所以我认为线程在 Python 里就是个鸡肋。尤其实在 3.x 系列里。

摘录参考资料: