Python 使用期物处理并发(实验Executor.map方法)

发布于:2025-07-25 ⋅ 阅读:(16) ⋅ 点赞:(0)

实验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 * 10def 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 秒,甚至会在第二个线程
开始之前就结束,不过具体情况因人而异。
❸ 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 实例创建。


网站公告

今日签到

点亮在社区的每一天
去签到