Brian Quinlan 是 concurrent.futures 包的贡献者,他在 PyCon Australia 2010 上所做的“The Future Is Soon!”(http://www.pyvideo.org/video/480/pyconau-2010--the-future-is-soon)演讲对这个包做了介绍。Quinlan 演讲时没用幻灯片,而是直接在 Python 控制台中输入代码,以此说明这个库的用途。作为引子,他在演讲中推荐了 XKCD 漫画家和程序员 Randall Munroe 制作的一个视频,Randall 在这个视频中对 Google Maps 发起了 DoS 攻击(非有意为之),绘制一个彩色地图,显示他驾车绕城的路线。这个库的正式介绍文件是“PEP 3148—futures—execute computations asynchronously”(https://www.python.org/dev/peps/pep-3148/)。在这个 PEP 中,Quinlan 写道,concurrent.futures 库“受 Java 的 java.util.concurrent 包影响很大”。
Jan Palach 写的 Parallel Programming with Python(Packt 出版社)一书介绍了几个并发编程的工具,包括 concurrent.futures、threading 和 multiprocessing 库。除了标准库之外,这本书还讨论了 Celery(http://celery.readthedocs.org/en/latest/getting-started/introduction.html)。这是一个任务队列,用于把工作分配给多个线程和进程,甚至是不同的设备。在 Django 社区中,为了减轻繁重任务的负担(例如,把生成 PDF 的工作交给其他进程,防止 HTTP 响应延迟生成),Celery 可能是使用最广泛的系统。
Beazley 与 Jones 的著作《Python Cookbook(第 3 版)中文版》有多个使用 concurrent.futures 的诀窍,首先是“11.12 理解事件驱动型 I/O”。“12.7 创建线程池”展示了一个简单的 TCP 回显服务器,“12.8 实现简单的并行编程”提供了一个特别实用的示例:借助 ProcessPoolExecutor 实例分析一整个目录中使用 gzip 压缩的 Apache 日志文件。这本书的第 12 章对线程做了更多介绍,特别值得一提的是“12.10 定义一个 Actor 任务”,这个诀窍演示了参与者模型:通过传递消息协调多个线程的可行方式。
Brett Slatkin 写的《Effective Python:编写高质量 Python 代码的 59 个有效方法》一书中有一章探讨了并发的多个话题,包括:协程;使用 concurrent.futures 库处理线程和进程;不使用 ThreadPoolExecutor 类,而使用锁和队列做线程编程。
Micha Gorelick 与 Ian Ozsvald 写的 High Performance Python(O'Reilly 出版社)和 Doug Hellmann 写的《Python 标准库》都涵盖了线程和进程。
若想了解不使用线程或回调的现代并发方式,推荐阅读 Paul Butcher 写的《七周七并发模型》。12 我喜欢这本书的副标题“When Threads Unravel”(线程束手无策之时)。这本书的第 1 章简单介绍了线程和锁,后面的六章探讨了不同语言(不包括 Python、Ruby 和 JavaScript)为并发编程提供的现代化替代方案。
12该书已由人民邮电出版社出版,书号:978-7-115-38606-9。——编者注
如果对 GIL 感兴趣,请先阅读 Python 文档中的“Python Library and Extension FAQ”(“Can't we get rid of the Global Interpreter Lock?”,https://docs.python.org/3/faq/library.html#id18)。Guido van Rossum 写的“It isn't Easy to Remove the GIL”(http://www.artima.com/weblogs/viewpost.jsp?thread=214235)和 Jesse Noller(multiprocessing 包的贡献者)写的“Python Threads and the Global Interpreter Lock”(http://jessenoller.com/2009/02/01/python-threads-and-the-global-interpreter-lock/)也值得一读。此外,David Beazley 在“Understanding the Python GIL”(http://www.dabeaz.com/GIL/)中详细探讨了 GIL 的内部运作。13 在这次演讲的第 54 张幻灯片中(http://www.dabeaz.com/python/UnderstandingGIL.pdf),Beazley 得出了一些令人担忧的结果,例如,使用 Python 3.2 引入的新 GIL 算法做基准测试时,他发现处理时间增加了 20 倍。不过,Beazley 似乎使用一个空的 while True: pass 循环模拟 CPU 密集型工作,而现实中不会这样做。在 Beazley 提交的缺陷报告中,根据 Antoine Pitrou(实现新 GIL 算法的人)的评论(http://bugs.python.org/issue7946#msg223110),这个问题与工作负载没有太大关系。
13感谢 Lucas Brunialti 把这个演讲的链接发给我。
GIL 是实际存在的问题,而且短时间内不可能消失,不过 Jesse Noller 和 Richard Oudkerk 开发了一个库,能让 CPU 密集型应用轻松地绕开这个问题——multiprocessing 包。这个包在多个进程中模拟 threading 模块的 API,而且支持基础设施的锁、队列、管道、共享内存,等等。这个包由“PEP 371—Addition of the multiprocessing package to the standard library”(https://www.python.org/dev/peps/pep-0371/)引入。这个包的官方文档是个 93KB 的 .rst 文件(大约 63 页),是 Python 标准库文档中最长的一章。多进程是 concurrent.futures.ProcessPoolExecutor 类的基础。
对于 CPU 密集型和数据密集型并行处理,现在有个新工具可用——分布式计算引擎 Apache Spark(https://spark.apache.org/)。Spark 在大数据领域发展势头强劲,提供了友好的 Python API,支持把 Python 对象当作数据,如示例页面所示(https://spark.apache.org/examples.html)。
João S. O. Bueno 开发的 lelo 库(https://pypi.python.org/pypi/lelo)和 Nat Pryce 开发的 python-parallelize 库(https://github.com/npryce/python-parallelize)简洁且十分易于使用,它们的作用是使用多个进程处理并行任务。lelo 包定义了一个 @parallel 装饰器,可以应用到任何函数上,把函数变成非阻塞:调用被装饰的函数时,函数在一个新进程中执行。 Nat Pryce 开发的 python-parallelize 包提供了一个 parallelize 生成器,能把 for 循环分配给多个 CPU 执行。这两个包在内部都使用了 multiprocessing 模块。
杂谈
远离线程
并发是计算机科学中最难的概念之一(通常最好别去招惹它)。14
——David Beazley
Python 教练和科学狂人上面引自 David Beazley 的话与本章开头引自 Michele Simionato 的话明显矛盾,但我都同意。在大学学过一门并发课程之后(那门课把“并发编程”与管理线程和锁划上等号),我得出一个结论,我不该自己管理线程和锁,而应该管理内存分配和释放。线程和锁最好由懂行的系统程序员管理,他们有这种爱好,也有时间去管理(但愿如此)。
因此我觉得
concurrent.futures包很棒,它把线程、进程和队列视作服务的基础设施,不用自己动手直接处理。当然,这个包针对的是简单的作业,也就是所谓的“高度并行”问题(https://en.wikipedia.org/wiki/Embarrassingly_parallel)。可是,正如本章开头 Simionato 所说的那样,编写应用(而非操作系统或数据库服务器)时,遇到的大部分并发问题都属于这一种。对于并发程度不高的问题来说,线程和锁也不是解决之道。在操作系统层面,线程永远不会消失;不过,过去七年我觉得让人眼前一亮的编程语言(包括 Go、Elixir 和 Clojure)都对并发做了更好、更高层的抽象,正如《七周七并发模型》一书所述。Erlang(实现 Elixir 的语言)是典型示例,设计这门语言时彻底考虑到了并发。我对这门语言不感兴趣的原因很简单——句法丑陋。我被 Python 的句法宠坏了。
José Valim 是著名的 Ruby on Rails 核心贡献者,他设计的 Elixir 提供了友好而现代的句法。与 Lisp 和 Clojure 一样,Elixir 也实现了句法宏。这是把双刃剑。使用句法宏能实现强大的 DSL,可是衍生语言多起来之后,代码基会出现兼容问题,社区会分裂。大量涌现的宏导致 Lisp 没落,因为各种 Lisp 实现都使用独特难懂的方言。标准化的 Common Lisp 则开始复苏。我希望 José Valim 能引领 Elixir 社区,不要重蹈覆辙。
与 Elixir 类似,Go 也是一门充满新意的现代语言。可是,与 Elixir 相比,某些方面有点保守。Go 不支持宏,句法比 Python 简单。Go 也不支持继承和运算符重载,而且提供的元编程支持没有 Python 多。这些限制被认为是 Go 语言的特点,因为行为和性能更可预料。这对高并发来说是好事,而 Go 的重要使命是取代 C++、Java 和 Python。
虽然 Elixir 和 Go 在高并发领域是直接的竞争者,但是设计原理的不同则吸引了不同的用户群。这两门语言都可能蓬勃发展。可是纵观编程语言的历史,保守的语言更能吸引程序员。我希望自己能精通 Go 和 Elixir。
关于 GIL
GIL 简化了 CPython 解释器和 C 语言扩展的实现。得益于 GIL,Python 有很多 C 语言扩展——这绝对是如今 Python 如此受欢迎的主要原因之一。
多年以来,我一直觉得 GIL 导致 Python 线程几乎没有用武之地,只能开发一些玩具应用。直到发现标准库中每一个阻塞型 I/O 函数都会释放 GIL 之后,我才意识到 Python 线程特别适合在 I/O 密集型系统(鉴于我的工作经验,客户经常付费让我开发这种应用)中使用。
竞争对手对并发的支持
MRI(推荐使用的 Ruby 实现)也有 GIL,因此,Ruby 线程与 Python 线程受到同样的限制。相比之下,JavaScript 解释器则根本不支持用户层级的线程。在 JavaScript 中,只能通过回调式异步编程实现并发。我提到这些是因为,Ruby 和 JavaScript 是最能直接与 Python 竞争的通用动态编程语言。
在深谙并发的这一批新语言中,Go 和 Elixir 或许是最能蚕食 Python 的语言。不过,现在有
asyncio了。既然这么多人相信纯粹使用回调的 Node.js 平台可以做并发编程,那么asyncio生态系统成熟后,Python 赢回这些人能有多难呢?不过,这是下一章“杂谈”的话题。
14摘自 PyCon 2009 教程“A Curious Course on Coroutines and Concurrency”(http://www.dabeaz.com/coroutines/)的第 9 张幻灯片。