- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- Python 术语表
- Python 版本表
- 排版约定
- 使用代码示例
- 第一部分 序幕
- 第 1 章 Python 数据模型
- 第二部分 数据结构
- 第 2 章 序列构成的数组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第三部分 把函数视作对象
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第四部分 面向对象惯用法
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽象基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第五部分 控制流程
- 第 14 章 可迭代的对象、迭代器和生成器
- 14.1 Sentence 类第1版:单词序列
- 14.2 可迭代的对象与迭代器的对比
- 14.3 Sentence 类第2版:典型的迭代器
- 14.4 Sentence 类第3版:生成器函数
- 14.5 Sentence 类第4版:惰性实现
- 14.6 Sentence 类第5版:生成器表达式
- 14.7 何时使用生成器表达式
- 14.8 另一个示例:等差数列生成器
- 14.9 标准库中的生成器函数
- 14.10 Python 3.3 中新出现的句法:yield from
- 14.11 可迭代的归约函数
- 14.12 深入分析 iter 函数
- 14.13 案例分析:在数据库转换工具中使用生成器
- 14.14 把生成器当成协程
- 14.15 本章小结
- 14.16 延伸阅读
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第六部分 元编程
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 结语
- 延伸阅读
- 附录 A 辅助脚本
- Python 术语表
- 作者简介
- 关于封面
17.3 使用 concurrent.futures 模块启动进程
concurrent.futures 模块的文档副标题是“Launching parallel tasks”(执行并行任务)。这个模块实现的是真正的并行计算,因为它使用 ProcessPoolExecutor 类把工作分配给多个 Python 进程处理。因此,如果需要做 CPU 密集型处理,使用这个模块能绕开 GIL,利用所有可用的 CPU 核心。
ProcessPoolExecutor 和 ThreadPoolExecutor 类都实现了通用的 Executor 接口,因此使用 concurrent.futures 模块能特别轻松地把基于线程的方案转成基于进程的方案。
下载国旗的示例或其他 I/O 密集型作业使用 ProcessPoolExecutor 类得不到任何好处。这一点易于验证,只需把示例 17-3 中下面这几行:
def download_many(cc_list): workers = min(MAX_WORKERS, len(cc_list)) with futures.ThreadPoolExecutor(workers) as executor:
改成:
def download_many(cc_list): with futures.ProcessPoolExecutor() as executor:
对简单的用途来说,这两个实现 Executor 接口的类唯一值得注意的区别是,ThreadPoolExecutor.__init__ 方法需要 max_workers 参数,指定线程池中线程的数量。在 ProcessPoolExecutor 类中,那个参数是可选的,而且大多数情况下不使用——默认值是 os.cpu_count() 函数返回的 CPU 数量。这样处理说得通,因为对 CPU 密集型的处理来说,不可能要求使用超过 CPU 数量的职程。而对 I/O 密集型处理来说,可以在一个 ThreadPoolExecutor 实例中使用 10 个、100 个或 1000 个线程;最佳线程数取决于做的是什么事,以及可用内存有多少,因此要仔细测试才能找到最佳的线程数。
经过几次测试,我发现使用 ProcessPoolExecutor 实例下载 20 面国旗的时间增加到了 1.8 秒,而原来使用 ThreadPoolExecutor 的版本是 1.4 秒。主要原因可能是,我的电脑用的是四核 CPU,因此限制只能有 4 个并发下载,而使用线程池的版本有 20 个工作的线程。
ProcessPoolExecutor 的价值体现在 CPU 密集型作业上。我用两个 CPU 密集型脚本做了一些性能测试。
arcfour_futures.py
这个脚本(代码清单参见示例 A-7)纯粹使用 Python 实现 RC4 算法。我加密并解密了 12 个字节数组,大小从 149KB 到 384KB 不等。
sha_futures.py
这个脚本(代码清单参见示例 A-9)使用标准库中的 hashlib 模块(使用 OpenSSL 库实现)实现 SHA-256 算法。我计算了 12 个 1MB 字节数组的 SHA-256 散列值。
这两个脚本除了显示汇总结果之外,没有使用 I/O。构建和处理数据的过程都在内存中完成,因此 I/O 对执行时间没有影响。
我运行了 64 次 RC4 示例,48 次 SHA 示例,平均时间如表 17-1 所示。统计的时间中包含派生工作进程的时间。
表17-1:在配有Intel Core i7 2.7 GHz四核CPU的设备中,使用Python 3.4运行RC4和SHA示例,分别使用1~4个职程得到的时间和提速倍数
职程数 | 运行RC4示例的时间 | RC4示例的提速倍数 | 运行SHA示例的时间 | SHA示例的提速倍数 |
1 | 11.48s | 1.00× | 22.66s | 1.00× |
2 | 8.65s | 1.33× | 14.90s | 1.52× |
3 | 6.04s | 1.90× | 11.91s | 1.90× |
4 | 5.58s | 2.06× | 10.89s | 2.08× |
可以看出,对加密算法来说,使用 ProcessPoolExecutor 类派生 4 个工作的进程后(如果有 4 个 CPU 核心的话),性能可以提高两倍。
对那个纯粹使用 Python 实现的 RC4 示例来说,如果使用 PyPy 和 4 个职程,与使用 CPython 和 4 个职程相比,速度能提高 3.8 倍。以表 17-1 中使用 CPython 和一个职程的运行时间为基准,速度提升了 7.8 倍。
如果使用 Python 处理 CPU 密集型工作,应该试试 PyPy。使用 PyPy 运行 arcfour_futures.py 脚本,速度快了 3.8~5.1 倍;具体的倍数由职程的数量决定。我测试时使用的是 PyPy 2.4.0,这一版与 Python 3.2.5 兼容,因此标准库中有 concurrent.futures 模块。
下面通过一个演示程序来研究线程池的行为。这个程序会创建一个包含 3 个职程的线程池,运行 5 个可调用的对象,输出带有时间戳的消息。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论