异步如何工作?
在回答这个问题之前,我们需要了解一些基本术语,如果您已经知道其中任何一个,请跳过这些术语。
发电机 https://wiki.python.org/moin/Generators
生成器是允许我们暂停 python 函数执行的对象。用户策划的生成器是使用关键字实现的yield https://docs.python.org/3/reference/expressions.html#yield-expressions。通过创建一个包含以下内容的普通函数yield
关键字,我们将该函数转换为生成器:
>>> def test():
... yield 1
... yield 2
...
>>> gen = test()
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
如您所见,调用next() https://docs.python.org/3/library/functions.html#next生成器上的 导致解释器加载测试的框架,并返回yield
编辑值。呼唤next()
再次,导致帧再次加载到解释器堆栈中,并继续yield
ing另一个值。
到了第三次next()
被调用,我们的生成器完成了,并且StopIteration https://docs.python.org/3/library/exceptions.html#StopIteration被扔了。
与发电机通信
生成器的一个鲜为人知的功能是您可以使用两种方法与它们进行通信:send() https://docs.python.org/3/reference/expressions.html#generator.send and throw() https://docs.python.org/3/reference/expressions.html#generator.throw.
>>> def test():
... val = yield 1
... print(val)
... yield 2
... yield 3
...
>>> gen = test()
>>> next(gen)
1
>>> gen.send("abc")
abc
2
>>> gen.throw(Exception())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in test
Exception
致电后gen.send()
,该值作为返回值传递yield
关键词。
gen.throw()
另一方面,允许在生成器内部抛出异常,并在同一位置引发异常yield
被称为。
从生成器返回值
从生成器返回一个值,结果将该值放入StopIteration
例外。我们稍后可以从异常中恢复该值并将其用于我们的需要。
>>> def test():
... yield 1
... return "abc"
...
>>> gen = test()
>>> next(gen)
1
>>> try:
... next(gen)
... except StopIteration as exc:
... print(exc.value)
...
abc
看哪,一个新关键字:yield from
Python 3.4 添加了一个新关键字:yield from https://docs.python.org/3/reference/expressions.html#yield-expressions。该关键字允许我们做的就是传递任何next()
, send()
and throw()
进入最内层的嵌套生成器。如果内部生成器返回一个值,它也是yield from
:
>>> def inner():
... inner_result = yield 2
... print('inner', inner_result)
... return 3
...
>>> def outer():
... yield 1
... val = yield from inner()
... print('outer', val)
... yield 4
...
>>> gen = outer()
>>> next(gen)
1
>>> next(gen) # Goes inside inner() automatically
2
>>> gen.send("abc")
inner abc
outer 3
4
我写过一篇文章 https://towardsdatascience.com/cpython-internals-how-do-generators-work-ba1c4405b4bc进一步阐述这个话题。
把它们放在一起
引入新关键字后yield from
在Python 3.4中,我们现在能够在生成器内部创建生成器,就像隧道一样,将数据从最里面的生成器来回传递到最外面的生成器。这为发电机产生了新的含义——协程.
协程是可以在运行时停止和恢复的函数。在 Python 中,它们是使用以下方式定义的async def https://docs.python.org/3/reference/compound_stmts.html#coroutine-function-definition关键词。就像发电机一样,它们也使用自己的形式yield from
这是await https://docs.python.org/3/reference/expressions.html#await。前async
and await
在 Python 3.5 中引入,我们以与创建生成器完全相同的方式创建协程(使用yield from
代替await
).
async def inner():
return 1
async def outer():
await inner()
就像所有迭代器和生成器都实现__iter__()
方法,所有协程都实现__await__()
这让他们每次都能继续await coro
叫做。
有一个不错的顺序图 https://docs.python.org/3.5/_images/tulip_coro.png在 - 的里面Python 文档 https://docs.python.org/3.5/library/asyncio-task.html#example-chain-coroutines你应该检查一下。
在 asyncio 中,除了协程函数之外,我们还有 2 个重要的对象:tasks and futures.
Futures https://docs.python.org/3/library/asyncio-task.html#asyncio.Future
期货是具有__await__()
方法实施,他们的工作是保持一定的状态和结果。状态可以是以下之一:
- PENDING - future 没有任何结果或异常集。
- 取消 - 未来被取消使用
fut.cancel()
- FINISHED - 未来已经完成,或者通过使用结果集fut.set_result() https://docs.python.org/3/library/asyncio-task.html#asyncio.Future.set_result或通过使用异常设置fut.set_exception() https://docs.python.org/3/library/asyncio-task.html#asyncio.Future.set_exception
正如您所猜测的,结果可以是返回的 Python 对象,也可以是可能引发的异常。
Another 重要的的特征future
对象,是它们包含一个名为的方法add_done_callback() https://docs.python.org/3/library/asyncio-task.html#asyncio.Future.add_done_callback。此方法允许任务完成后立即调用函数 - 无论是引发异常还是完成。
Tasks https://docs.python.org/3/library/asyncio-task.html#task
任务对象是特殊的 future,它包裹着协程,并与最里面和最外面的协程进行通信。每次协程await
是一个未来,未来会一路回到任务(就像yield from
),任务收到它。
接下来,任务将自身与未来联系起来。它通过调用来做到这一点add_done_callback()
关于未来。从现在开始,如果未来将要完成,通过取消、传递异常或传递 Python 对象作为结果,任务的回调将被调用,并且它将恢复存在。
Asyncio
我们必须回答的最后一个紧迫问题是——IO 是如何实现的?
在 asyncio 的深处,我们有一个事件循环。任务的事件循环。事件循环的工作是在每次任务准备好时调用它们,并将所有工作协调到一台工作机器中。
事件循环的 IO 部分构建在一个名为的关键函数之上select https://docs.python.org/3/library/select.html#module-select。 Select 是一个阻塞函数,由底层操作系统实现,允许在套接字上等待传入或传出数据。接收到数据后,它会唤醒,并返回已接收数据的套接字,或准备写入的套接字。
当您尝试通过 asyncio 通过套接字接收或发送数据时,下面实际发生的情况是首先检查套接字是否有任何可以立即读取或发送的数据。如果它是.send()
缓冲区已满,或者.recv()
缓冲区为空,套接字已注册到select
函数(只需将其添加到列表之一,rlist
for recv
and wlist
for send
)和适当的函数await
是一个新创建的future
对象,绑定到该套接字。
当所有可用任务都在等待 future 时,事件循环调用select
并等待。当其中一个套接字有传入数据时,或其send
缓冲区耗尽后,asyncio 检查绑定到该套接字的未来对象,并将其设置为完成。
现在所有的魔法都发生了。未来已经完成,之前添加的任务add_done_callback()
复活并呼唤.send()
在恢复最里面的协程的协程上(因为await
链),并且您从附近的缓冲区读取新接收到的数据。
再次方法链,以防出现recv()
:
-
select.select
waits.
- 一个准备好的套接字,并返回数据。
- 来自套接字的数据被移动到缓冲区中。
-
future.set_result()
叫做。
- 添加自身的任务
add_done_callback()
现在已经醒了。
- 任务调用
.send()
在协程上,它一直进入最里面的协程并唤醒它。
- 数据正在从缓冲区中读取并返回给我们谦虚的用户。
总之,asyncio 使用生成器功能,允许暂停和恢复功能。它用yield from
允许从最里面的生成器到最外面的生成器来回传递数据的功能。它使用所有这些来在等待 IO 完成时停止函数执行(通过使用操作系统select
功能)。
最重要的是?当一个功能暂停时,另一个功能可能会运行并与精致的结构交错,这是异步的。