【问题标题】:Why is it that only asynchronous functions can yield in asynchronous code?为什么只有异步函数才能在异步代码中产生?
【发布时间】:2020-04-20 18:02:25
【问题描述】:

"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的函数不能阻塞?

【问题讨论】:

  • 你了解那篇文章中的“产量”是什么意思吗?
  • 也许不是。他是不是在yield 关键字的意义上使用“yield”这个词,例如生成器迭代器?
  • 不。这意味着让其他代码运行。
  • @RobertHarvey 也许你可以写一个简短的答案指出yield的含义,这样问题就解决了?
  • @user4815162342 根据 Harvey 的有用评论和一些扩展的思考/阅读,我自己写了一个扩展的答案。希望听起来是正确的。

标签: python asynchronous python-asyncio yield


【解决方案1】:

逐行进行:

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

在机器上运行的程序按照进程进行组织。每个进程可能有一个或多个线程。线程和进程一样,由操作系统调度(并可中断)。在这种情况下,“yield”一词的意思是“让其他代码运行”。当工作在多个线程之间分配时,函数很容易“屈服”:操作系统暂停在一个线程中运行的代码,在不同的线程中运行一些代码,暂停,返回,并在第一个线程上运行更多,等等在。通过这种方式在线程之间切换,就实现了并发。

在这个执行模型中,被挂起的代码是同步的还是异步的并不重要。线程的代码是逐行运行的,所以同步函数的基本假设——在运行一行代码和下一行代码之间没有发生变化——是没有违反。

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

在此上下文中的“异步代码”是指与多线程应用程序执行相同工作的单线程应用程序,不同之处在于它通过使用线程的异步函数来实现并发,而不是不同的线程之间拆分工作。在这种执行模型中,您的解释器而不是操作系统负责根据需要在函数之间切换以实现并发。

在此执行模型中,在位于异步函数内部的同步函数中间暂停工作是不安全的。这样做意味着在运行同步函数的过程中运行一些其他代码,打破同步函数所做的“逐行”假设。

因此,解释器只会在同步子函数之间等待暂停异步函数的执行,而不是在一个子函数内。这就是异步代码中的同步函数不能产生的语句的意思:一旦同步函数开始运行,它就必须完成。

这意味着例如 writer.write 方法不能阻塞。

writer.write 方法是同步的,因此,在异步程序中运行时,不会中断。如果这个方法被阻塞,它不仅会阻塞它内部运行的异步函数,还会阻塞整个程序。那会很糟糕。 writer.write 通过写入写入缓冲区并立即返回来避免阻塞程序。

严格来说,writer.write可以屏蔽,只是不建议这样做。

如果您需要在异步函数内部进行阻塞,那么正确的方法是 await 另一个异步函数。这就是例如await writer.drain() 确实如此。这将异步阻塞:当这个特定函数保持阻塞时,它会正确让步给其他可以运行的函数。

【讨论】:

    【解决方案2】:

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

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

    因此,同步函数不能简单地作为定义的问题产生。限制的重要之处在于,使用正常(同步)call 调用的函数不能产生:它的调用者没有准备好处理这样的交互。 (如果它无论如何都会发生什么当然是相同的“被遗忘的await”。)这也会影响重构:如果不更改其所有客户端(并使它们如果它们还没有,那么也是异步的)。 (这类似于 Haskell 中 all I/O 的工作方式,因为它会影响执行任何功能的 type。)

    请注意,yield允许作为普通生成器的角色,即使在异步函数中也可以与普通 for 一起使用,但这只是调用者必须期望相同的一般事实protocol 作为被调用者:如果增强生成器(“旧式”协程)与 for 一起使用,它只会从每个 (yield) 中获取 None,如果 async函数与for 一起使用,它产生的等待对象可能会在发送None 时中断它们

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

    【讨论】:

      猜你喜欢
      • 2023-03-10
      • 1970-01-01
      • 2021-01-15
      • 1970-01-01
      • 1970-01-01
      • 2019-04-27
      • 1970-01-01
      • 2019-02-15
      • 2020-10-04
      相关资源
      最近更新 更多