引言
现在,asyncio 已成为 Python 社区中的热门话题,并且名副其实——它提供了一种非常出色的处理 I/O 密集型程序的方法!在我探索 asyncio 的过程中,我起初并不太明白它的工作原理。但随着深入学习,我意识到 asyncio 实际上是在 Python 生成器的基础上增加了一层非常便利的封装。
本文[1]中,我将展示如何仅用 Python 生成器来构建一个 asyncio 的简化模型。接着,我会演示如何利用 await 魔法方法,将示例代码改写为使用 async 和 await 关键字。最终,我会将我的简化版本替换为官方的 asyncio 库。通过这个过程,我相信你将对 asyncio 的神奇之处有一个更深入的理解。
生成器
如果您已经熟悉生成器,请跳过这一部分,但如果您不熟悉,那么 asyncio 就是基于它构建的,因此了解它们的工作原理非常重要。
首先,生成器之所以存在,是因为它们可以让你的代码更加内存高效。想象一下,如果您有以下循环:
代码语言:javascript复制for i in range(100_000_000):
print(i)
如果 range 函数不是以生成器的形式存在,而是返回一个列表让你去遍历,那么类似上面例子的代码在内存使用上将非常浪费,因为你需要创建一个包含高达 1 亿个元素的列表。但是,由于 range 在 Python 3 或更高版本中是一个生成器,你只需在需要时逐个生成数字,而不必将整个序列一次性加载到内存中。
创建生成器有多种方法,但本文将重点介绍生成器函数。生成器函数的声明与其他函数无异,但它使用 yield 语句来逐个返回数据。这个 yield 语句将普通函数转变为一个可以按需暂停和恢复执行状态的生成器,这通过调用 next(iterator) 来实现。
例如,下面是一个生成器函数的示例:
代码语言:javascript复制def generator():
yield 'hello'
yield 'world'
iterator = generator()
当您调用生成器时,它不会像 Python 通常那样运行函数内部的代码,而是会看到yield 关键字,因此返回一个生成器对象。一旦我们有了生成器对象,我们就可以调用 next(iterator),它将运行函数的代码,直到第一个/下一个yield语句:
代码语言:javascript复制print(next(iterator)) # Output: hello
print(next(iterator)) # Output: world
如果我们尝试再次调用 next(iterator),生成器将引发 StopIteration 异常,因为生成器函数中不再有yield 语句。 Python 生成器的另一个很酷的功能是yield from,它允许生成器调用子生成器或可迭代对象,使您能够创建生成器链!
代码语言:javascript复制def generator():
yield 'hello'
def another_generator():
yield from generator()
iterable = another_generator()
print(next(iterable)) # Output: hello
生成器的功能远不止我提到的这些,例如生成器推导式,它与列表推导式类似,但使用的是圆括号而非方括号,还有通过 iterator.send(value) 方法向生成器传递数据的功能。不过,对于本文而言,最关键的是理解生成器能够让函数在执行过程中暂停和恢复,同时保持其内部状态。
事件循环
事件循环是 asyncio 的心脏,负责驱动和管理所有当前任务的执行,我们将首先用生成器来模拟它。虽然 asyncio 的事件循环是用 C 语言实现的,但我们可以将其想象成一个容器,里面存放着所有活跃的任务。目前,我们把这些任务看作是生成器对象。事件循环管理器会依次遍历容器中的任务,并通过调用 next(task) 函数来执行它们。当任务执行到 I/O 操作,比如等待(sleep)时,它会使用 yield 关键字来挂起当前的执行流程,并将控制权交还给事件循环,后者随后会转向执行队列中的下一个任务。
举个例子,我们有两个任务,它们首先打印出自己的任务编号,然后执行 yield 操作,这会导致它们的执行被挂起。因为事件循环管理器负责调用 next() 函数,所以在任务执行 yield 后,管理器会重新获得控制权,并继续执行循环中的下一个任务。
代码语言:javascript复制def task1():
while True:
print('Task 1')
yield
def task2():
while True:
print('Task 2')
yield
event_loop = [task1(), task2()]
while True:
for task in event_loop:
next(task)
随后,该代码的输出将如下所示,并且将永远持续下去,因为由于 while True 循环,两个生成器函数都永远不会完成。
代码语言:javascript复制Task 1
Task 2
Task 1
Task 2
…