asyncio实际上如何运作?

时间:2018-02-27 09:48:11

标签: python python-3.x python-asyncio

这个问题是由我的另一个问题推动的:How to await in cdef?

网上有大量关于asyncio的文章和博客文章,但它们都非常肤浅。我找不到有关asyncio如何实际实现的信息,以及I / O异步的原因。我试图阅读源代码,但它是成千上万行不是最高级别的C代码,其中很多处理辅助对象,但最重要的是,很难在Python语法和它将翻译的C代码之间建立连接成。

Asycnio自己的文档甚至没那么有用。没有关于它是如何工作的信息,只有关于如何使用它的一些指导,这些指南有时也会产生误导/写得很差。

我熟悉Go的coroutines实现,并且希望Python做同样的事情。如果是这种情况,我在上面链接的帖子中出现的代码就可以了。既然没有,我现在正试图找出原因。到目前为止,我最好的猜测如下,请纠正我错在哪里:

  1. 表单async def foo(): ...的过程定义实际上被解释为继承coroutine的类的方法。
  2. 也许,async def实际上被await语句拆分为多个方法,其中调用这些方法的对象能够跟踪到目前为止通过执行所做的进度。
  3. 如果上述情况属实,那么,基本上,协程的执行归结为一些全局管理器调用协程对象的方法(循环?)。
  4. 全局管理器以某种方式(如何?)了解I / O操作何时由Python(仅?)代码执行,并且能够在当前执行方法放弃控制之后选择一个待执行的协程方法执行(命中)在await声明中。
  5. 换句话说,这是我尝试将某些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解释器如何才能意识到这一点?

3 个答案:

答案 0 :(得分:77)

asyncio如何工作?

在回答这个问题之前,我们需要了解一些基本术语,如果您已经知道一些基本术语,请跳过这些基本术语。

Generators

生成器是允许我们暂停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中引入asyncawait之前,我们以与生成生成器完全相同的方式创建了协程(使用yield from而不是await)。

async def inner():
    return 1

async def outer():
    await inner()

协程像每个实现__iter__()方法的迭代器或生成器一样,协程实现__await__(),使它们在每次调用await coro时继续执行。

sequence diagram中有一个不错的Python docs,您应该检查一下。

在异步中,除了协程功能外,我们还有两个重要的对象:任务未来

Futures

未来是实现了__await__()方法的对象,其工作是保持一定的状态和结果。状态可以是以下之一:

  1. 待处理-未来未设置任何结果或例外。
  2. 已取消-未来已使用fut.cancel()取消了
  3. 完成-未来已经完成,要么通过使用fut.set_result()的结果集,要么通过使用fut.set_exception()的异常集

就像您猜到的那样,结果可以是将返回的Python对象,也可能是引发异常的对象。

future对象的另一个重要功能是,它们包含名为 add_done_callback() 的方法。此方法允许在任务完成后立即调用函数-无论是引发异常还是完成。

Tasks

任务对象是特殊的Future,它们包裹着协程,并与最内部和最外部的协程进行通信。每当协程await表示未来时,就将未来一路传递回任务(就像在yield from中一样),任务就会收到它。

接下来,任务将自身绑定到未来。通过在将来调用add_done_callback()来实现。从现在开始,如果将来能够实现,通过取消,传递异常或传递Python对象作为结果,任务的回调将被调用,并将恢复到存在状态。

异步

我们必须回答的最后一个迫切问题是-IO如何实现?

深入异步内部,我们有一个事件循环。任务的事件循环。事件循环的工作是在每次准备就绪时调用任务,并将所有工作协调到单个工作机器中。

事件循环的IO部分基于称为 select 的单个关键功能构建。 Select是一种阻止功能,由下面的操作系统实现,该功能允许在套接字上等待传入或传出数据。接收到数据后,它将唤醒,并返回接收到数据的套接字或准备写入的套接字。

当您尝试通过asyncio通过套接字接收或发送数据时,下面实际发生的情况是,首先检查套接字是否具有可以立即读取或发送的任何数据。如果.send()缓冲区已满,或者.recv()缓冲区为空,则将套接字注册到select函数(只需将其添加到列表之一rlist中) recvwlist代表send),相应的功能await是一个新创建的future对象,与该套接字绑定。

当所有可用任务都在等待将来时,事件循环将调用select并等待。当其中一个套接字有传入数据,或者send缓冲区耗尽时,asyncio会检查与该套接字绑定的将来对象,并将其设置为完成。

现在所有的魔术都发生了。未来已成定局,在add_done_callback()之前添加自己的任务会重新出现,并在协程中调用.send(),以恢复最内层的协程(由于{{1} }链),然后从附近的缓冲区读取新接收到的数据,并将其溢出到该缓冲区。

再次使用方法链(如果使用await

  1. recv()等待。
  2. 已准备好的套接字,其中包含数据。
  3. 套接字中的数据被移到缓冲区中。
  4. select.select被呼叫。
  5. 添加了future.set_result()的任务现在被唤醒。
  6. 任务在协程中调用add_done_callback(),该步程将一直进入最内层的协程并唤醒它。
  7. 正在从缓冲区读取数据并返回给我们谦虚的用户。

总而言之,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集中,因此它将从该点继续暂停。

换句话说:

  1. 默认情况下,所有都发生在同一个帖子中。

  2. 事件循环负责安排协同程序并在它们等待的任何内容(通常是通常会阻塞或超时的IO调用)准备就绪时将其唤醒。

  3. 有关协程驱动事件循环的见解,我推荐Dave Beazley的this talk,他在现场观众面前演示从头开始编写事件循环。

答案 2 :(得分:2)

这一切归结为asyncio正在解决的两个主要挑战:

  • 如何在单个线程中执行多个I / O?
  • 如何实施合作多任务处理?

第一点的答案已经存在了很长一段时间,被称为select loop。在python中,它是在selectors module

中实现的

第二个问题与coroutine的概念有关,即可以停止执行并稍后恢复的功能。在python中,使用generatorsyield 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的异步版本。