为什么只有异步函数才能产生异步代码?

时间:2020-01-02 00:07:43

标签: python asynchronous python-asyncio yield

在文章"I'm not feeling the async pressure"中,阿明·罗纳赫(Armin Ronacher)提出以下看法:

在线程代码中,任何函数都可以产生。在异步代码中,只有异步函数可以。例如,这意味着writer.write方法无法阻止。

此观察是参考以下代码示例进行的:

from asyncio import start_server, run

async def on_client_connected(reader, writer):
    while True:
        data = await reader.readline()
        if not data:
            break
        writer.write(data)

async def server():
    srv = await start_server(on_client_connected, '127.0.0.1', 8888)
    async with srv:
        await srv.serve_forever()

run(server())

我不明白此评论。具体来说:

  • 为什么在异步函数内部不能使用yield同步函数?
  • yield与阻止执行有什么关系?为什么不能yield不能阻塞的功能?

2 个答案:

答案 0 :(得分:1)

逐行浏览:

在线程代码中,任何函数都可以产生结果。

计算机上运行的程序是按照进程进行组织的。每个进程可能具有一个或多个线程。线程(如进程)由操作系统调度(并可由操作系统中断)。在此上下文中,单词“ yield”表示“让其他代码运行”。当工作被拆分到多个线程之间时,函数很容易“屈服”:操作系统将一个线程中运行的代码挂起,在另一个线程中运行一些代码,将其挂起,然后返回,然后在第一个线程上进行更多工作,等等。上。通过以这种方式在线程之间切换,可以实现并发。

在此执行模型中,暂停的代码是同步还是异步无关紧要。线程内的代码 是逐行运行的,因此,同步功能的基本假设-在运行一行代码和下一行代码之间没有发生任何变化-没有违反。

在异步代码中,只有异步函数可以。

在此上下文中,“异步代码”表示单线程应用程序,其功能与多线程应用程序相同,不同之处在于它是通过在线程内使用异步函数 而不是线程来实现并发性的。在 个不同线程之间分配工作。在此执行模型中,解释器(而不是操作系统)负责根据需要在功能之间进行切换以实现并发性。

在此执行模型中,将工作挂在位于异步函数内部的同步函数中间是不安全的。这样做将意味着在运行同步功能的同时运行其他一些代码,从而破坏了同步功能所做的“逐行”假设。

结果是,解释器将仅等待同步子功能之间的异步功能的执行暂停,而不会等待一个子功能。这就是异步代码中的同步函数无法产生的语句的含义:一旦同步函数开始运行,它就必须完成。

例如,这意味着writer.write方法无法阻止。

writer.write方法是同步的,因此,在异步程序中运行时,该方法是不可中断的。如果要阻塞此方法,则它将不仅阻塞正在其内部运行的异步函数,还将阻塞整个程序。那将是不好的。 writer.write通过改为写入写缓冲区并立即返回来避免阻塞程序。

严格来说,writer.write 可以阻止,这样做是不明智的。

如果您需要在异步函数内部进行阻塞,则正确的方法是await另一个异步函数。这就是例如await writer.drain()可以。这将异步阻止:尽管此特定功能仍处于阻止状态,但可以正确地屈服于其他可以运行的功能。

答案 1 :(得分:1)

此处的“收益”是指cooperative multitasking(尽管在一个过程中而不是在它们之中)。在Python编程的async / await风格的上下文中,异步函数是defined in terms of Python预先存在的生成器支持:如果函数阻塞(通常用于I / O),则其所有功能正在执行await主叫方被挂起(带有不可见的yield / yield from,确实是生成器类型)。任何生成器的实际调用都是使用其next方法;该功能实际上返回

每个调用程序(必须是大多数程序员从未编写过的某种驱动程序)必须参与该方法的工作:任何未挂起的函数将突然由驱动程序负责决定什么在等待调用完成的功能时执行下一步。这种异步的“传染性”方面被称为“color”;这可能会产生问题,例如,当人们忘记await像这样的其他呼叫一样,看起来正确的协程呼叫时,就会出现问题。 (存在async / await语法是通过将函数隐式转换为状态机来最大程度地减少并发对程序结构的破坏,但是这种歧义仍然存在。)是一件好事:异步函数可以在await时准确中断,因此直接可以推断出数据结构的一致性。

因此,同步功能不能仅仅因为定义而产生。限制的引入实际上是使用普通(同步)调用调用的函数无法产生:其调用者未准备好处理此类交互。 (无论如何都会发生的情况当然是相同的“被遗忘的await”。)这也会影响重构:在不更改其所有客户端(以及所有客户端)的情况下,函数不能更改为异步的如果还没有的话,也可以使它们异步)。 (这与 all I / O在Haskell中的工作方式类似,因为它会影响执行任何功能的任何功能的 type 。)

请注意,即使在异步函数中,yield作为与普通for一起使用的普通生成器,也可以允许使用,但这只是调用者的一般事实必须期望与被调用方具有相同的 protocol :如果for使用了增强的生成器(“老式”协程),它只会从每个{{ 1}},如果将None函数与(yield)一起使用,则会产生等待项,这些等待项可能在发送 async时中断。

与线程或所谓的 stackful 协程或纤维的区别在于,调用者不需要特殊的恢复支持,因为实际的函数调用只是< strong>在恢复线程/光纤之前不会返回。 (在线程情况下,内核还选择何时来恢复它。)从这种意义上讲,这些方法更易于使用,但是对于纤程来说,“潜入”任何函数的暂停的能力部分地由于需要对该函数指定 arguments 来折衷,以向其告知用于注册自身的用户空间调度程序(除非您愿意为此使用全局变量…)。另一方面,线程的开销比光纤还要高,这在大量线程运行时很重要。