- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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.4 实验 Executor.map 方法
若想并发运行多个可调用的对象,最简单的方式是使用示例 17-3 中见过的 Executor.map 方法。示例 17-6 中的脚本演示了 Executor.map 方法的某些运作细节。这个脚本的输出在示例 17-7 中。
示例 17-6 demo_executor_map.py:简单演示 ThreadPoolExecutor 类的 map 方法
from time import sleep, strftime from concurrent import futures def display(*args): ➊ print(strftime('[%H:%M:%S]'), end=' ') print(*args) def loiter(n): ➋ msg = '{}loiter({}): doing nothing for {}s...' display(msg.format('\t'*n, n, n)) sleep(n) msg = '{}loiter({}): done.' display(msg.format('\t'*n, n)) return n * 10 ➌ def main(): display('Script starting.') executor = futures.ThreadPoolExecutor(max_workers=3) ➍ results = executor.map(loiter, range(5)) ➎ display('results:', results) ➏ display('Waiting for individual results:') for i, result in enumerate(results): ➐ display('result {}: {}'.format(i, result)) main()
❶ 这个函数的作用很简单,把传入的参数打印出来,并在前面加上 [HH:MM:SS] 格式的时间戳。
❷ loiter 函数什么也没做,只是在开始时显示一个消息,然后休眠 n 秒,最后在结束时再显示一个消息;消息使用制表符缩进,缩进的量由 n 的值确定。
❸ loiter 函数返回 n * 10,以便让我们了解收集结果的方式。
❹ 创建 ThreadPoolExecutor 实例,有 3 个线程。
❺ 把五个任务提交给 executor(因为只有 3 个线程,所以只有 3 个任务会立即开始:loiter(0)、loiter(1) 和 loiter(2));这是非阻塞调用。
❻ 立即显示调用 executor.map 方法的结果:一个生成器,如示例 17-7 中的输出所示。
❼ for 循环中的 enumerate 函数会隐式调用 next(results),这个函数又会在(内部)表示第一个任务(loiter(0))的 _f 期物上调用 _f.result() 方法。result 方法会阻塞,直到期物运行结束,因此这个循环每次迭代时都要等待下一个结果做好准备。
我建议你运行示例 17-6,看着结果逐渐显示出来。此外,还可以修改 ThreadPoolExecutor 构造方法的 max_workers 参数,以及 executor.map 方法中 range 函数的参数;或者自己挑选几个值,以列表的形式传给 map 方法,得到不同的延迟。
示例 17-7 是运行示例 17-6 得到的输出示例。
示例 17-7 示例 17-6 中 demo_executor_map.py 脚本的运行示例
$ python3 demo_executor_map.py [15:56:50] Script starting. ➊ [15:56:50] loiter(0): doing nothing for 0s... ➋ [15:56:50] loiter(0): done. [15:56:50] loiter(1): doing nothing for 1s... ➌ [15:56:50] loiter(2): doing nothing for 2s... [15:56:50] results: <generator object result_iterator at 0x106517168> ➍ [15:56:50] loiter(3): doing nothing for 3s... ➎ [15:56:50] Waiting for individual results: [15:56:50] result 0: 0 ➏ [15:56:51] loiter(1): done. ➐ [15:56:51] loiter(4): doing nothing for 4s... [15:56:51] result 1: 10 ➑ [15:56:52] loiter(2): done. ➒ [15:56:52] result 2: 20 [15:56:53] loiter(3): done. [15:56:53] result 3: 30 [15:56:55] loiter(4): done. ➓ [15:56:55] result 4: 40
❶ 这次运行从 15:56:50 开始。
❷ 第一个线程执行 loiter(0),因此休眠 0 秒,甚至会在第二个线程开始之前就结束,不过具体情况因人而异。7
7具体情况因人而异:对线程来说,你永远不知道某一时刻事件的具体排序;有可能在另一台设备中会看到 loiter(1) 在 loiter(0) 结束之前开始,这是因为 sleep 函数总会释放 GIL。因此,即使休眠 0 秒, Python 也可能会切换到另一个线程。
❸ loiter(1) 和 loiter(2) 立即开始(因为线程池中有三个职程,可以并发运行三个函数)。
❹ 这一行表明,executor.map 方法返回的结果(results)是生成器;不管有多少任务,也不管 max_workers 的值是多少,目前不会阻塞。
❺ loiter(0) 运行结束了,第一个职程可以启动第四个线程,运行 loiter(3)。
❻ 此时执行过程可能阻塞,具体情况取决于传给 loiter 函数的参数:results 生成器的 __next__ 方法必须等到第一个期物运行结束。此时不会阻塞,因为 loiter(0) 在循环开始前结束。注意,这一点之前的所有事件都在同一刻发生——15:56:50。
❼ 一秒钟后,即 15:56:51,loiter(1) 运行完毕。这个线程闲置,可以开始运行 loiter(4)。
❽ 显示 loiter(1) 的结果:10。现在,for 循环会阻塞,等待 loiter(2) 的结果。
❾ 同上:loiter(2) 运行结束,显示结果;loiter(3) 也一样。
❿ 2 秒钟后 loiter(4) 运行结束,因为 loiter(4) 在 15:56:51 时开始,休眠了 4 秒。
Executor.map 函数易于使用,不过有个特性可能有用,也可能没用,具体情况取决于需求:这个函数返回结果的顺序与调用开始的顺序一致。如果第一个调用生成结果用时 10 秒,而其他调用只用 1 秒,代码会阻塞 10 秒,获取 map 方法返回的生成器产出的第一个结果。在此之后,获取后续结果时不会阻塞,因为后续的调用已经结束。如果必须等到获取所有结果后再处理,这种行为没问题;不过,通常更可取的方式是,不管提交的顺序,只要有结果就获取。为此,要把 Executor.submit 方法和 futures.as_completed 函数结合起来使用,像示例 17-4 中那样。17.5.2 节会继续讨论这种方式。
executor.submit 和 futures.as_completed 这个组合比 executor.map 更灵活,因为 submit 方法能处理不同的可调用对象和参数,而 executor.map 只能处理参数不同的同一个可调用对象。此外,传给 futures.as_completed 函数的期物集合可以来自多个 Executor 实例,例如一些由 ThreadPoolExecutor 实例创建,另一些由 ProcessPoolExecutor 实例创建。
下一节根据新的需求继续实现下载国旗的示例,这一次不使用 executor.map 方法,而是迭代 futures.as_completed 函数返回的结果。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论