协程和任务都可以用在 await 表达式中。那么它们之间有什么共同点呢?为了理解这一点,我们需要了解未来和可等待对象。你通常不需要直接使用未来,但理解它们对于理解 asyncio 的内部工作机制至关重要。由于一些 API 返回未来,我们将在本书的其余部分引用它们。
future 是一个包含单个值的 Python 对象,你预计在未来某个时刻获得这个值,但可能尚未获得。通常,当你创建一个 future 时,它还没有任何值,因为它还不存在。在这种状态下,它被认为是不完整的、未解决的,或者简单地说,还未完成。然后,一旦你获得了结果,就可以设置 future 的值。这将完成 future;此时,我们可以认为它已完成,并从中提取结果。为了理解未来的基本原理,让我们尝试创建一个,设置它的值,然后重新提取该值。
from asyncio import Futuremy_future = Future()print(f'Is my_future done? {my_future.done()}')my_future.set_result(42)print(f'Is my_future done? {my_future.done()}')print(f'What is the result of my_future? {my_future.result()}')
我们可以通过调用其构造函数来创建一个 future。此时,future 没有任何结果,所以调用其 done 方法将返回 False。然后我们用其 set_result 方法设置 future 的值,这将把 future 标记为 done。或者,如果我们有一个想要设置在 future 上的异常,我们可以调用 set_exception。
注意 我们不能在结果设置之前调用 方法,因为 方法在调用时会抛出无效状态异常。
未来也可以用在 await 表达式中。如果我们 await 一个 future,我们就是在说:“暂停,直到 future 有值可以处理,一旦我有了值,就唤醒我并让我处理它。”
为了理解这一点,让我们考虑一个返回 future 的网络请求的例子。一个返回 future 的请求应该会立即完成,但由于请求需要一些时间,future 还未定义。然后,稍后,一旦请求完成,结果就会被设置,然后我们就可以访问它。如果你过去使用过 JavaScript,这个概念类似于承诺。在 Java 世界中,它们被称为可完成的未来。
from asyncio import Futureimport asynciodef make_request() -> Future: future = Future() asyncio.create_task(set_future_value(future)) # ❶ return futureasync def set_future_value(future) -> None: await asyncio.sleep(1) # ❷ future.set_result(42)async def main(): future = make_request() print(f'Is the future done? {future.done()}') value = await future # ❸ print(f'Is the future done? {future.done()}') print(value)asyncio.run(main())
❶ 创建一个任务来异步设置未来的值。❷ 等待 1 秒后再设置未来的值。❸ 暂停 ,直到未来的值被设置。
在上面的示例中,我们定义了一个函数 make_request。在这个函数中,我们创建了一个 future,并创建了一个 task,该 task 将在 1 秒后异步设置 future 的结果。然后,在 main 函数中,我们调用 make_request。当我们调用它时,我们会立即得到一个没有结果的 future;因此,它是未完成的。然后,我们 await 这个 future。await 这个 future 会暂停 main 1 秒,等待 future 的值被设置。一旦完成,value 将是 42,future 也将是 done。
在 asyncio 世界中,你很少需要直接处理未来。尽管如此,你可能会遇到一些返回未来的 asyncio API,或者可能需要处理基于回调的代码,这可能需要未来。你可能还需要自己阅读或调试一些 asyncio API 代码。这些 asyncio API 的实现严重依赖于未来,因此最好对它们的工作原理有一个基本的了解。
未来和任务之间有着很强的关系。事实上,task 直接继承自 future。你可以把 future 想象成一个将来才会有的值。你可以把 task 想象成协程和 future 的结合。当我们创建一个 task 时,我们是在创建一个空的 future 并运行协程。然后,当协程以异常或结果完成时,我们会设置 future 的结果或异常。
鉴于未来和任务之间的关系,任务和协程之间是否有类似的关系?毕竟,所有这些类型都可以用在 await 表达式中。
它们的共同点是 Awaitable 抽象基类。这个类定义了一个抽象的双下划线方法 __await__。我们不会深入探讨如何创建自己的 awaitables,但任何实现了 __await__ 方法的对象都可以用在 await 表达式中。协程直接继承自 Awaitable,futures 也是如此。任务则扩展了 futures,这给出了图 2.5 所示的继承图。
从现在开始,我们将开始将可以在 await 表达式中使用的对象称为可等待对象。你经常会在 asyncio 文档中看到“可等待对象”这个词,因为许多 API 方法并不关心你传入的是协程、任务还是未来。
现在我们理解了协程、任务和未来的基础知识,我们如何评估它们的性能呢?到目前为止,我们只是理论化了它们的耗时。为了使事情更加严谨,让我们添加一些功能来测量执行时间。