在本书的前几章,我们学过如何创建多个任务来并发运行协程。为此,我们用了 asyncio.create_task,然后像下面这样 await 任务:
import asyncioasync def main() -> None: task_one = asyncio.create_task(delay(1)) task_two = asyncio.create_task(delay(2)) await task_one await task_two
这种方式对简单场景(比如一次启动一两个协程)是可行的。但在一个需要同时发起数百、数千甚至更多网络请求的世界里,这种写法会变得冗长且混乱。
我们可能会想用 for 循环或列表推导式来让它更顺滑,如下所示。但如果不写对,这种方法可能会出问题。
import asynciofrom util import async_timed, delay@async_timed()async def main() -> None: delay_times = [3, 3, 3] [await asyncio.create_task(delay(seconds)) for seconds in delay_times]asyncio.run(main())
理想情况下,我们希望 delay 任务能并发运行,所以期望 main 方法大约 3 秒内完成。但实际上,却花了 9 秒,因为所有操作都是顺序进行的:
starting <function main at 0x10f14a550> with args () {}starting <function delay at 0x10f7684c0> with args (3,) {}sleeping for 3 second(s)finished sleeping for 3 second(s)finished <function delay at 0x10f7684c0> in 3.0008 second(s)starting <function delay at 0x10f7684c0> with args (3,) {}sleeping for 3 second(s)finished sleeping for 3 second(s)finished <function delay at 0x10f7684c0> in 3.0009 second(s)starting <function delay at 0x10f7684c0> with args (3,) {}sleeping for 3 second(s)finished sleeping for 3 second(s)finished <function delay at 0x10f7684c0> in 3.0020 second(s)finished <function main at 0x10f14a550> in 9.0044 second(s)
问题出得很微妙。原因是我们在创建任务后立刻就 await 了。这意味着我们会暂停列表推导式和 main 协程,直到每一个 delay 任务都完成为止。在这种情况下,我们实际上同一时间只运行一个任务,而不是并发运行多个任务。修复方法很简单,虽然稍微啰嗦点。我们可以用一个列表推导式来创建所有任务,然后再用另一个列表推导式来 await。这样就能让所有任务并发运行。
import asynciofrom util import async_timed, delay@async_timed()async def main() -> None: delay_times = [3, 3, 3] tasks = [asyncio.create_task(delay(seconds)) for seconds in delay_times] [await task for task in tasks]asyncio.run(main())
这段代码一次性创建了多个任务并放入 tasks 列表。一旦所有任务都创建完毕,我们再用另一个列表推导式来等待它们的完成。这样之所以有效,是因为 create_task 会立即返回,我们不会在所有任务创建完毕前做任何 await 操作。这确保了运行时间最多只受 delay_times 中最大延迟时间的影响,因此大约需要 3 秒:
starting <function main at 0x10d4e1550> with args () {}starting <function delay at 0x10daff4c0> with args (3,) {}sleeping for 3 second(s)starting <function delay at 0x10daff4c0> with args (3,) {}sleeping for 3 second(s)starting <function delay at 0x10daff4c0> with args (3,) {}sleeping for 3 second(s)finished sleeping for 3 second(s)finished <function delay at 0x10daff4c0> in 3.0029 second(s)finished sleeping for 3 second(s)finished <function delay at 0x10daff4c0> in 3.0029 second(s)finished sleeping for 3 second(s)finished <function delay at 0x10daff4c0> in 3.0029 second(s)finished <function main at 0x10d4e1550> in 3.0031 second(s)
虽然这个方法能达到目的,但仍有不足之处。第一,它需要多行代码,你必须刻意记住要把任务创建和 await 分开。第二,它不够灵活,如果其中一个协程比其他几个快得多,你还是会困在第二个列表推导式里,一直等到所有协程都完成。虽然在某些情况下这可以接受,但有时我们希望更灵敏,一有结果就立刻处理。第三,也是可能最大的问题,是异常处理。如果某个协程抛出了异常,它会在你 await 失败的任务时被抛出。这意味着你无法处理那些成功完成的任务,因为那个异常会直接中断你的执行。
asyncio 提供了一些方便的函数来应对所有这些问题。当需要并发运行多个任务时,推荐使用这些函数。在接下来的章节中,我们会研究其中一些,看看如何在并发执行多个网络请求时使用它们。