返回介绍

17.7 延伸阅读

发布于 2024-02-05 21:59:47 字数 6101 浏览 0 评论 0 收藏 0

Brian Quinlan 是 concurrent.futures 包的贡献者,他在 PyCon Australia 2010 上所做的“The Future Is Soon!”演讲对这个包做了介绍。Quinlan 演讲时没用幻灯片,而是直接在 Python 控制台中输入代码,以此说明这个库的用途。作为引子,他在演讲中推荐了 XKCD 漫画家和程序员 Randall Munroe 制作的一个视频,Randall 在这个视频中对 Google Maps 发起了 DoS 攻击(非有意为之),绘制一个彩色地图,显示他驾车绕城的路线。这个库的正式介绍文件是“PEP 3148—futures—execute computations asynchronously”。在这个 PEP 中,Quinlan 写道,concurrent.futures 库“受 Java 的 java.util.concurrent 包影响很大”。

Jan Palach 写的 Parallel Programming with Python(Packt 出版社)一书介绍了几个并发编程的工具,包括 concurrent.futures、threading 和 multiprocessing 库。除了标准库之外,这本书还讨论了 Celery。这是一个任务队列,用于把工作分配给多个线程和进程,甚至是不同的设备。在 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?”)。Guido van Rossum 写的“It isn't Easy to Remove the GIL”和 Jesse Noller(multiprocessing 包的贡献者)写的“Python Threads and the Global Interpreter Lock”也值得一读。此外,David Beazley 在“Understanding the Python GIL”中详细探讨了 GIL 的内部运作。13 在这次演讲的第 54 张幻灯片中,Beazley 得出了一些令人担忧的结果,例如,使用 Python 3.2 引入的新 GIL 算法做基准测试时,他发现处理时间增加了 20 倍。不过,Beazley 似乎使用一个空的 while True: pass 循环模拟 CPU 密集型工作,而现实中不会这样做。在 Beazley 提交的缺陷报告中,根据 Antoine Pitrou(实现新 GIL 算法的人)的评论,这个问题与工作负载没有太大关系。

13感谢 Lucas Brunialti 把这个演讲的链接发给我。

GIL 是实际存在的问题,而且短时间内不可能消失,不过 Jesse Noller 和 Richard Oudkerk 开发了一个库,能让 CPU 密集型应用轻松地绕开这个问题——multiprocessing 包。这个包在多个进程中模拟 threading 模块的 API,而且支持基础设施的锁、队列、管道、共享内存,等等。这个包由“PEP 371—Addition of the multiprocessing package to the standard library”引入。这个包的官方文档是个 93KB 的 .rst 文件(大约 63 页),是 Python 标准库文档中最长的一章。多进程是 concurrent.futures.ProcessPoolExecutor 类的基础。

对于 CPU 密集型和数据密集型并行处理,现在有个新工具可用——分布式计算引擎 Apache Spark。Spark 在大数据领域发展势头强劲,提供了友好的 Python API,支持把 Python 对象当作数据,如示例页面所示。

João S. O. Bueno 开发的 lelo和 Nat Pryce 开发的 python-parallelize简洁且十分易于使用,它们的作用是使用多个进程处理并行任务。lelo 包定义了一个 @parallel 装饰器,可以应用到任何函数上,把函数变成非阻塞:调用被装饰的函数时,函数在一个新进程中执行。 Nat Pryce 开发的 python-parallelize 包提供了一个 parallelize 生成器,能把 for 循环分配给多个 CPU 执行。这两个包在内部都使用了 multiprocessing 模块。

杂谈

远离线程

并发是计算机科学中最难的概念之一(通常最好别去招惹它)。14

——David Beazley
Python 教练和科学狂人

上面引自 David Beazley 的话与本章开头引自 Michele Simionato 的话明显矛盾,但我都同意。在大学学过一门并发课程之后(那门课把“并发编程”与管理线程和锁划上等号),我得出一个结论,我不该自己管理线程和锁,而应该管理内存分配和释放。线程和锁最好由懂行的系统程序员管理,他们有这种爱好,也有时间去管理(但愿如此)。

因此我觉得 concurrent.futures 包很棒,它把线程、进程和队列视作服务的基础设施,不用自己动手直接处理。当然,这个包针对的是简单的作业,也就是所谓的“高度并行”问题。可是,正如本章开头 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”的第 9 张幻灯片。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文