这个问题是由我的另一个问题推动的:How to await in cdef?
网上有大量关于asyncio
的文章和博客文章,但它们都非常肤浅。我找不到有关asyncio
如何实际实现的信息,以及I / O异步的原因。我试图阅读源代码,但它是成千上万行不是最高级别的C代码,其中很多处理辅助对象,但最重要的是,很难在Python语法和它将翻译的C代码之间建立连接成。
Asycnio自己的文档甚至没那么有用。没有关于它是如何工作的信息,只有关于如何使用它的一些指导,这些指南有时也会产生误导/写得很差。
我熟悉Go的coroutines实现,并且希望Python做同样的事情。如果是这种情况,我在上面链接的帖子中出现的代码就可以了。既然没有,我现在正试图找出原因。到目前为止,我最好的猜测如下,请纠正我错在哪里:
async def foo(): ...
的过程定义实际上被解释为继承coroutine
的类的方法。async def
实际上被await
语句拆分为多个方法,其中调用这些方法的对象能够跟踪到目前为止通过执行所做的进度。 await
声明中。换句话说,这是我尝试将某些asyncio
语法“贬低”为更容易理解的内容:
async def coro(name):
print('before', name)
await asyncio.sleep()
print('after', name)
asyncio.gather(coro('first'), coro('second'))
# translated from async def coro(name)
class Coro(coroutine):
def before(self, name):
print('before', name)
def after(self, name):
print('after', name)
def __init__(self, name):
self.name = name
self.parts = self.before, self.after
self.pos = 0
def __call__():
self.parts[self.pos](self.name)
self.pos += 1
def done(self):
return self.pos == len(self.parts)
# translated from asyncio.gather()
class AsyncIOManager:
def gather(*coros):
while not every(c.done() for c in coros):
coro = random.choice(coros)
coro()
我的猜测应该证明是正确的:那我就有问题了。在这种情况下,I / O实际上是如何发生的?在一个单独的线程?整个翻译是否被暂停,I / O发生在翻译之外? I / O究竟是什么意思?如果我的python过程调用了C open()
过程,并且它又向内核发送了中断,放弃了对它的控制,那么Python解释器如何知道这一点并且能够继续运行其他代码,而内核代码实际上是I / O直到它唤醒最初发送中断的Python程序?原则上Python解释器如何才能意识到这一点?
答案 0 :(得分:77)
在回答这个问题之前,我们需要了解一些基本术语,如果您已经知道一些基本术语,请跳过这些基本术语。
生成器是允许我们暂停python函数执行的对象。用户指定的生成器使用关键字yield
实现。通过创建包含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()
会使解释器加载测试的帧,并返回经过yield
的值。再次调用next()
,使该帧再次加载到解释器堆栈中,并继续yield
并输入另一个值。
第三次调用next()
时,我们的生成器已完成,并且抛出了StopIteration
。
生成器的一个鲜为人知的功能是您可以使用两种方法与它们通信:send()
和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()
允许在生成器中引发Exception,但在同一位置调用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
。该关键字允许我们执行的操作是将任何next()
,send()
和throw()
传递到最内层的嵌套生成器中。如果内部生成器返回一个值,那么它也是yield from
的返回值:
>>> def inner():
... print((yield 2))
... return 3
...
>>> def outer():
... yield 1
... val = yield from inner()
... print(val)
... yield 4
...
>>> gen = outer()
>>> next(gen)
1
>>> next(gen)
2
>>> gen.send("abc")
abc
3
4
在Python 3.4中引入新的关键字yield from
之后,我们现在能够在生成器内部创建生成器,就像生成隧道一样,将数据从最内层生成器来回传递到最外层生成器。这为生成器产生了新的含义-协程。
协程是可以在运行时停止和恢复的功能。在Python中,它们是使用 async def
关键字定义的。就像生成器一样,它们也使用自己的yield from
形式,即 await
。在Python 3.5中引入async
和await
之前,我们以与生成生成器完全相同的方式创建了协程(使用yield from
而不是await
)。
async def inner():
return 1
async def outer():
await inner()
协程像每个实现__iter__()
方法的迭代器或生成器一样,协程实现__await__()
,使它们在每次调用await coro
时继续执行。
sequence diagram中有一个不错的Python docs,您应该检查一下。
在异步中,除了协程功能外,我们还有两个重要的对象:任务和未来。
未来是实现了__await__()
方法的对象,其工作是保持一定的状态和结果。状态可以是以下之一:
fut.cancel()
取消了fut.set_result()
的结果集,要么通过使用fut.set_exception()
的异常集就像您猜到的那样,结果可以是将返回的Python对象,也可能是引发异常的对象。
future
对象的另一个重要功能是,它们包含名为 add_done_callback()
的方法。此方法允许在任务完成后立即调用函数-无论是引发异常还是完成。
任务对象是特殊的Future,它们包裹着协程,并与最内部和最外部的协程进行通信。每当协程await
表示未来时,就将未来一路传递回任务(就像在yield from
中一样),任务就会收到它。
接下来,任务将自身绑定到未来。通过在将来调用add_done_callback()
来实现。从现在开始,如果将来能够实现,通过取消,传递异常或传递Python对象作为结果,任务的回调将被调用,并将恢复到存在状态。
我们必须回答的最后一个迫切问题是-IO如何实现?
深入异步内部,我们有一个事件循环。任务的事件循环。事件循环的工作是在每次准备就绪时调用任务,并将所有工作协调到单个工作机器中。
事件循环的IO部分基于称为 select
的单个关键功能构建。 Select是一种阻止功能,由下面的操作系统实现,该功能允许在套接字上等待传入或传出数据。接收到数据后,它将唤醒,并返回接收到数据的套接字或准备写入的套接字。
当您尝试通过asyncio通过套接字接收或发送数据时,下面实际发生的情况是,首先检查套接字是否具有可以立即读取或发送的任何数据。如果.send()
缓冲区已满,或者.recv()
缓冲区为空,则将套接字注册到select
函数(只需将其添加到列表之一rlist
中) recv
和wlist
代表send
),相应的功能await
是一个新创建的future
对象,与该套接字绑定。
当所有可用任务都在等待将来时,事件循环将调用select
并等待。当其中一个套接字有传入数据,或者send
缓冲区耗尽时,asyncio会检查与该套接字绑定的将来对象,并将其设置为完成。
现在所有的魔术都发生了。未来已成定局,在add_done_callback()
之前添加自己的任务会重新出现,并在协程中调用.send()
,以恢复最内层的协程(由于{{1} }链),然后从附近的缓冲区读取新接收到的数据,并将其溢出到该缓冲区。
再次使用方法链(如果使用await
:
recv()
等待。select.select
被呼叫。future.set_result()
的任务现在被唤醒。add_done_callback()
,该步程将一直进入最内层的协程并唤醒它。总而言之,asyncio使用生成器功能,该功能允许暂停和恢复功能。它使用.send()
功能,可以将数据从最内层生成器来回传递到最外层。在等待IO完成时(通过使用OS yield from
函数),它使用所有这些命令来停止函数执行。
还有最好的吗?当一个功能暂停时,另一个功能可能会运行并与精致的面料(即异步面料)交错。
答案 1 :(得分:6)
您的coro
desugaring在概念上是正确的,但稍微不完整。
await
无条件暂停,但仅在遇到阻止呼叫时才会暂停。它是如何知道呼叫阻塞的?这是由等待的代码决定的。例如,套接字读取的等待实现可能会被淘汰:
def read(sock, n):
# sock must be in non-blocking mode
try:
return sock.recv(n)
except EWOULDBLOCK:
event_loop.add_reader(sock.fileno, current_task())
return SUSPEND
在真正的asyncio中,equivalent code修改Future
的状态而不是返回魔术值,但概念是相同的。当适当地适应类似生成器的对象时,上面的代码可以是await
。
在来电方面,当您的协程包含:
data = await read(sock, 1024)
它脱离了接近的东西:
data = read(sock, 1024)
if data is SUSPEND:
return SUSPEND
self.pos += 1
self.parts[self.pos](...)
熟悉生成器的人倾向于用yield from
描述上面的内容,它会自动执行暂停。
挂起链一直持续到事件循环,它注意到协程被挂起,将其从可运行集中删除,然后继续执行可运行的协同程序(如果有的话)。如果没有协同程序可运行,则循环在select()
中等待,直到协同程序感兴趣的文件描述符为IO做好准备。 (事件循环维护文件描述符到协同程序的映射。)
在上面的示例中,一旦select()
告诉事件循环sock
是可读的,它会将coro
重新添加到runnable集中,因此它将从该点继续暂停。
换句话说:
默认情况下,所有都发生在同一个帖子中。
事件循环负责安排协同程序并在它们等待的任何内容(通常是通常会阻塞或超时的IO调用)准备就绪时将其唤醒。
有关协程驱动事件循环的见解,我推荐Dave Beazley的this talk,他在现场观众面前演示从头开始编写事件循环。
答案 2 :(得分:2)
这一切归结为asyncio正在解决的两个主要挑战:
第一点的答案已经存在了很长一段时间,被称为select loop。在python中,它是在selectors module。
中实现的第二个问题与coroutine的概念有关,即可以停止执行并稍后恢复的功能。在python中,使用generators和yield from语句实现协同程序。这就是隐藏在async/await syntax背后的东西。
此answer中的更多资源。
编辑:解决有关goroutines的评论:
asyncio中与goroutine最接近的等价物实际上不是协程,而是一项任务(请参阅documentation中的差异)。在python中,协程(或生成器)对事件循环或I / O的概念一无所知。它只是一个函数,可以在保持当前状态的同时使用yield
停止执行,因此可以在以后恢复。 yield from
语法允许以透明的方式链接它们。
现在,在asyncio任务中,链最底部的协程总是最终产生future。然后这个未来泡沫化到事件循环,并融入内部机器。当未来被一些其他内部回调设置为完成时,事件循环可以通过将未来发送回协程链来恢复任务。
编辑:解决帖子中的一些问题:
在这种情况下,I / O实际上是如何发生的?在一个单独的线程?整个翻译是否被暂停,I / O发生在翻译之外?
不,线程中没有任何反应。 I / O总是由事件循环管理,主要通过文件描述符。然而,这些文件描述符的注册通常被高级协同程序隐藏,使得脏工作为你。
I / O究竟是什么意思?如果我的python程序调用了C open()程序,并且它又向内核发送了中断,放弃了对它的控制,Python解释器如何知道这一点并且能够继续运行其他代码,而内核代码实际上是I / O,直到它唤醒最初发送中断的Python程序?原则上Python解释器如何才能意识到这一点?
I / O是任何阻止呼叫。在asyncio中,所有I / O操作都应该通过事件循环,因为正如您所说,事件循环无法知道在某些同步代码中正在执行阻塞调用。这意味着你不应该在协同程序的上下文中使用同步open
。相反,请使用专用库aiofiles,它提供open
的异步版本。