【问题标题】:How does a coroutine control where it yields to?协程如何控制它的屈服点?
【发布时间】:2019-09-06 07:24:41
【问题描述】:

生成器与函数/子例程的不同之处在于,如果重复调用,它会从它们上次返回的行恢复,而不是从顶部重新开始。

虽然生成器总是将控制权交还给其调用例程,但apparently* 协程的区别仅在于控制它们将执行转移到何处的能力。

这是如何工作的,它与子程序调用有何不同(它也暂时暂停当前子程序的执行,同时控制立即执行的位置)?


* “所有这些使得生成器函数与协程非常相似;它们产生多次,它们有多个入口点,并且它们的执行可以暂停。唯一的区别 是生成器函数无法控制在它产生后应该继续执行的位置;控制权总是转移到生成器的调用者。" -- Python 手册,v2.5-3.7(自从引入了 yield-expressions 以来) .

此外,Conway 最初的 (1963) description 协程涉及它们使用对称语法相互传递控制,类似于对输入/输出函数的调用。 (类似地,Knuth The Art of Computer Programming 3rd Ed 给出了国际象棋游戏中每个玩家的协程示例,描述 “无法区分哪个是其他” 因为两个协程都像子程序一样调用另一个。)

【问题讨论】:

  • 并非如此。生成器是从不查找 yield 表达式返回值的协程。
  • 究竟是什么让你认为协程能够控制它们将执行转移到哪里?
  • @martineau 有问题引用的来源;我已编辑以包含相关报价。
  • benjimin:感谢您的澄清。不幸的是,我仍然不太明白该怎么做。我认为这可能与协程能够yield 另一个协程(例如yield from another_coroutine())有关,但我不确定。很高兴看到文档正在讨论的示例。

标签: python coroutine


【解决方案1】:

这里的部分混淆是“协程”一词经常被松散地应用。对协程的最初描述暗示了一组代码块,它们可以对称地、有选择地转移控制权,以便从它们离开的地方相互恢复。尽管如此,它只是一个低层次的结构(在一个像子程序调用堆栈这样的概念仍在开发的时代)并且留下了一些未指定的细节。特别是,如果协程即使在嵌套的子程序调用中也能够暂停和恢复,则需要为每个协程管理一个单独的内存堆栈(以避免碎片),因此它们将构成协作多任务(又名纤程或轻量级/微型/绿色/user 线程,在单个抢占式 OS 线程中启用一般并发)。或者,如果协程仅从其顶层(不是嵌套例程)相互传递控制,那么它们都可以共享现有堆栈。

如 python 文档中所述,yield-expressions(即生成器例程中的y = yield x)几乎完全满足无堆栈协程的概念,但以不对称的方式。调用代码和生成器例程都能够放弃控制以恢复对方(从上次所在的位置),并相互传递和接收值。但是,语法是不对称的(调用者使用例如send,而被调用者使用yield),因此这些关系形成了无循环的层次结构或树形图(因此嵌套的子生成器不能直接向其祖父生成/发送调用者没有干预父生成器的显式协调)。

可以通过从“蹦床”(事件循环)启动 python 生成器并让生成器产生令牌来指示循环接下来迭代哪个生成器来实现对称无堆栈协同程序。 (例如,这可以允许使用协程来模拟多个玩家轮流在像 UNO 这样的纸牌游戏中。)或者,可以让“调度程序”事件循环负责决定接下来要恢复哪个协程,例如让不同“协程”执行的任务异步进行。

此外,“协程”可以协调其所有事件循环通信,以委托给嵌套的子“协程”,直到该子“协程”终止。这是一种子程序调用的协程模拟。在 python 中,awaityield from 是这个委托的语法糖。

异步函数(Python 所谓的“本机协程”)有效地使用await 表达式来构造一种调用堆栈(可能是以堆上链接的等待对象的形式)。在某个阶段,当前最内层嵌套的等待对象将把控制权交还给调度程序,调度程序可能决定将控制权交给一个完全独立的协程堆栈。在 python 中,由库(例如 curioasyncio)来实现主事件循环和与事件循环通信的原始等待对象。

(也存在异步生成器。它们选择异常系统以便能够将控制权转移回父例程,同时还能够将让步委托给事件循环。)

协程是否控制它们屈服的位置?

在 comp-sci 理论中,是的;但在 python 中,只有一种。

如果只有两个例程同时执行(例如,一个生成器加上迭代它的例程),那么这个问题无论如何都是没有实际意义的。协程的原始用例是同时运行数据处理管道的连续阶段,即通过嵌套生成器进行流式传输(与多通道算法相比,最小化延迟和临时存储);对于这样的链式拓扑,嵌套 python 生成器的非对称语法确实可以选择接下来恢复哪个例程。

然而,一般来说,python 协程(尤其是异步函数)需要一个事件循环来控制接下来将执行转移到哪里。通常异步函数无法控制何时恢复。

这与子程序调用相比如何?

调用和让步都是暂停当前例程的两种方式。

在一系列子程序调用中,调用者被挂起,但被调用者每次从第一行重新开始。 (被调用者不能在不终止自身的情况下将控制权返回给调用者。)

一个包含两个协程的算法可以在 python 中表示为一个生成器(使用yield 表达式)和一个子程序(例如调用send 方法);从这个意义上说,子程序可以是协程。

“本机协程”(异步函数)实际上似乎旨在模仿普通子例程,只是支持并发(但没有强加并行性或抢占,可能减轻确保例程可重入/线程安全的负担)。它们有助于任务管理; await 表达式指定了任务之间的依赖关系网络,并鼓励以程序风格编写单个任务(和子任务)(而不是显式传递回调等)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-06-10
    • 2020-03-06
    • 2013-02-24
    • 1970-01-01
    • 1970-01-01
    • 2011-02-09
    • 1970-01-01
    相关资源
    最近更新 更多