我们前面的例子没有使用任何慢速操作,主要是为了帮助我们学习协程的基本语法。要真正体会到 asyncio 的好处,并展示如何同时运行多个事件,我们需要引入一些长时间运行的操作。与其马上去做网络 API 或数据库查询(这些操作所需的时间是不确定的),我们不如先通过指定等待时间来模拟长时间运行的操作。我们将使用 asyncio.sleep 函数来实现这一点。
我们可以用 asyncio.sleep 让一个协程“睡”指定的秒数。这会暂停我们的协程一段时间,模拟出像数据库或网络 API 这类耗时调用的情况。
asyncio.sleep 本身就是一个协程,所以我们必须用 await 关键字来使用它。如果我们只是直接调用它,会得到一个协程对象。由于 asyncio.sleep 是一个协程,这意味着当一个协程等待它时,其他代码就能运行。
让我们来看一个简单的例子,如下所示,它会睡 1 秒,然后打印一条 'Hello World!' 消息。
import asyncioasync def hello_world_message() -> str: await asyncio.sleep(1) # ❶ return 'Hello World!'async def main() -> None: hello_world_message() # ❷ print(message)asyncio.run(main())
当我们运行这个应用程序时,程序会等待 1 秒,然后才打印出我们的 'Hello World!' 消息。因为 hello_world_message 是一个协程,我们用 asyncio.sleep 暂停它 1 秒,所以我们现在有 1 秒的时间可以并发运行其他代码。
接下来的几个例子中我们会频繁使用 sleep,所以让我们花点时间创建一个可重用的协程,它能帮我们睡觉并打印一些有用的信息。我们把这个协程叫做 delay。如下所示:
import asyncioasync def delay(delay_seconds: int) -> int: print(f'sleeping for {delay_seconds} second(s)') await asyncio.sleep(delay_seconds) print(f'finished sleeping for {delay_seconds} second(s)') return delay_seconds
delay 接收一个以秒为单位的持续时间参数,当函数睡完觉后,会把该整数返回给调用者。我们还会打印出睡眠开始和结束的时间。这有助于我们观察在协程被暂停时,是否有其他代码在并发运行。
为了在后续的代码示例中更方便地引用这个工具函数,我们将创建一个模块,以后在需要时导入。我们还会在这个模块中添加更多可重用的函数。我们把这个模块叫做 util,并将我们的 delay 函数放在一个名为 delay_functions.py 的文件里。我们还会添加一个 __init__.py 文件,里面写上以下这行,以便于漂亮地导入定时器:
from util.delay_functions import delay
从现在起,本书中只要需要用到 delay 函数,我们都会使用 from util import delay。现在我们有了一个可重用的 delay 协程,让我们把它和前面的 add_one 协程结合起来,看看是否能让我们的简单加法运算在 hello_world_message 被暂停的同时并发运行。
import asynciofrom util import delayasync def add_one(number: int) -> int: return number + 1async def hello_world_message() -> str: await delay(1) return 'Hello World!'async def main() -> None: message = await hello_world_message() # ❶ one_plus_one = await add_one(1) # ❷ print(one_plus_one) print(message)asyncio.run(main())
当我们运行这段代码时,1 秒过后,两个函数调用的结果才会被打印出来。我们真正想要的效果是,add_one(1) 的值能在 hello_world_message() 并发运行的同时立即打印出来。那为什么这段代码没有达到这个效果呢?答案是:await 会暂停我们当前的协程,并且不会执行该协程内的任何其他代码,直到 await 表达式给我们返回一个值。由于 hello_world_message 函数需要 1 秒才能返回一个值,因此 main 协程会被暂停 1 秒。在这种情况下,我们的代码表现得就像它是顺序执行的一样。这种行为如图 2.2 所示。
main 和 hello_world 都在等待 delay(1) 完成时暂停了。一旦完成,main 就恢复并可以执行 add_one。
我们希望摆脱这种顺序模型,让 add_one 与 hello_world 并发运行。为了实现这一点,我们需要引入一个叫任务的概念。