返回介绍

17.4 实验 Executor.map 方法

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

若想并发运行多个可调用的对象,最简单的方式是使用示例 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 技术交流群。

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

发布评论

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