到目前为止,我们一直在谈论应用程序的大致运行时间,但没有实际测量。为了真正理解和分析性能,我们需要引入一些代码来为我们追踪这些时间。
作为第一次尝试,我们可以包装每一个 await 语句并追踪协程的开始和结束时间:
import asyncioimport timeasync def main(): start = time.time() await asyncio.sleep(1) end = time.time() print(f'Sleeping took {end - start} seconds')asyncio.run(main())
然而,当有多个 await 语句和任务需要追踪时,这种方法很快就会变得混乱。更好的方法是想出一种可重用的方法来追踪任何协程完成所需的时间。我们可以通过创建一个装饰器来实现这一点,它会为我们运行 await 语句(如示例 2.16 所示)。我们将这个装饰器命名为 async_timed。
什么是装饰器?装饰器是 Python 中的一种模式,允许我们在不更改函数代码的情况下为其添加功能。我们可以“拦截”函数的调用,并在调用前后应用任何装饰器代码。装饰器是一种处理交叉关注点的方法。下面的示例展示了样本装饰器。
import functoolsimport timefrom typing import Callable, Anydef async_timed(): def wrapper(func: Callable) -> Callable: @functools.wraps(func) async def wrapped(*args, **kwargs) -> Any: print(f'starting {func} with args {args}{kwargs}') start = time.time() try: return await func(*args, **kwargs) finally: end = time.time() total = end - start print(f'finished {func} in {total:.4f} second(s)') return wrapped return wrapper
在这个装饰器中,我们创建了一个名为 wrapped 的新协程。这是一个围绕我们原始协程的包装器,它接收其参数 *args 和 **kwargs,调用 await 语句,然后返回结果。我们用一条消息包围这个 await 语句,表示我们开始运行函数,另一条消息表示我们结束运行函数,以与我们之前的开始时间和结束时间示例相同的方式跟踪开始和结束时间。现在,如示例 2.17 所示,我们可以把这个注解应用到任何协程上,每次运行它时,我们都会看到它花了多长时间。
import asyncio@async_timed()async 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@async_timed()async def main(): task_one = asyncio.create_task(delay(2)) task_two = asyncio.create_task(delay(3)) await task_one await task_twoasyncio.run(main())
当我们运行上面的示例时,我们会看到类似以下的控制台输出:
starting <function main at 0x109111ee0> with args () {}starting <function delay at 0x1090dc700> with args (2,) {}starting <function delay at 0x1090dc700> with args (3,) {}finished <function delay at 0x1090dc700> in 2.0032 second(s)finished <function delay at 0x1090dc700> in 3.0003 second(s)finished <function main at 0x109111ee0> in 3.0004 second(s)
我们可以看到,我们的两个 delay 调用分别在大约 2 秒和 3 秒内完成,总共耗时 5 秒。但请注意,我们的 main 协程只用了 3 秒就完成了,因为我们是并发等待的。
我们将使用这个装饰器和产生的输出,贯穿接下来的几章,来说明我们的协程执行多长时间,以及它们何时开始和完成。这将让我们清楚地看到,通过并发执行我们的操作,我们在性能上取得了哪些提升。
为了在未来的代码示例中更方便地引用这个工具装饰器,让我们把这个装饰器添加到我们的 util 模块中。我们将定时器放在一个名为 async_timer.py 的文件中。我们还会在模块的 __init__.py 文件中添加一行,以便于漂亮地导入定时器:
from util.async_timer import async_timed
在本书的其余部分,每当需要使用定时器时,我们都会使用 from util import async_timed。
现在我们已经可以用装饰器来理解 asyncio 在并发运行任务时能提供的性能提升,我们可能会忍不住想在现有的应用程序中到处使用 asyncio。这虽然可行,但我们需要小心,不要陷入使用 asyncio 时常见的陷阱,这些陷阱可能会降低我们应用程序的性能。