11.1 多线程笔记

什么是多线程?它是在一个Python进程中将代码运行在不同的处理器上1的能力。这意味着代码的不同部分可以并行运行。

为什么需要多线程呢?最常见的场景如下。

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

(1)需要运行后台任务但不希望停止主线程的执行。例如,在图形用户界面场景下,主循环需要等待对事件的响应。

(2)需要将工作负载分布在几个CPU上。

所以,刚开始要解决这些问题的话,多线程似乎是扩展和并行化应用程序的好办法。当需要分散工作负载时,只需要为新的请求启动一个新线程而不用一次只能处理一个。

太棒了,搞定!让我们继续。

不,很抱歉!首先,如果你已经在Python领域中混了很久,那么你肯定遇到过GIL这个词,而且知道它多么讨厌。GIL是指Python全局解释锁(Global Interpreter Lock),当CPython2每次要执行字节码时都要先申请这个锁。但是,这意味着,如果试图通过多线程扩展应用程序,将总是被这个全局锁所限制。

所以尽管多线程看上去是一个理想的解决方案,但实际上我看到的大多数应用程序都很难获取到150%的CPU利用率,也就是使用1.5个核(core)。考虑到现如今计算节点通常至少有2个或4个核,这是很没面子的。这都归咎于GIL。

目前没有任何工作试图从CPython中移除GIL,因为考虑到实现和维护的难度大家都觉得不值得这么做。

然而,CPython只是Python的可用实现之一3。例如,Jython(http://www.jython.org/)就没有全局解释锁(http://www.jython.org/jythonbook/en/1.0/Concurrency.html),这意味着它可以有效地并行运行多个线程。遗憾的是,这些项目相对于CPython都非常滞后,所以实际上并不能作为目标平台来使用。

 注意

PyPy是另一个Python实现,但是是使用Python开发的(参见10.6节)。PyPy也有GIL,但目前有一个非常有意思的工作正在试图用基于STM(Software Transactional Memory,http://www.jython.org/jythonbook/en/1.0/Concurrency.html)的实现替换它。这对于未来构建和运行多线程软件是非常值得期待的变化。某些处理器正在试图提供硬件支持,而Linux内核的开发者也在寻求废弃内核锁的方法。这些都是积极的信号。

没有好的方案是不是我们又回到了最初的场景呢?并非如此,至少还有以下两种方案可用。

(1)如果需要运行后台任务,最容易的方式是基于事件循环构建应用程序。许多不同的Python模块都提供这一机制,甚至有一个标准库中的模块——asyncore,它是PEP 3156(https://www.python.org/dev/peps/pep-3156/)中标准化这一功能的成果。有些框架就是基于这一概念构建的,如Twisted(http://twistedmatrix.com/trac/)。最高级的框架应该提供基于信号量、计时器和文件描述符活动来访问事件,我们将在11.3节中进行讨论。

(2)如果需要分散工作负载,使用多进程会更简单有效,参见11.2节。

对于我们这些开发人员、普通人来说,这意味着我们在使用多线程时要三思。我在rebuildd(http://julien.danjou.info/projects/rebuildd)中使用多线程来分发作业,rebuildd是我多年前写的一个做Debian构建(build)的守护进程。尽管用线程去控制每个构建作业很方便,但我很快便掉进了并发陷阱中。如果有机会再做一次的话,我会使用基于异步事件处理或者多进程的方式来做,也就不用再担心这个问题了。

处理好多线程是很难的。其复杂程度意味着与其他方式相比它是bug的更大来源,而且考虑到通常能够获得的好处很少,所以最好不要在多线程上浪费太多精力。