Python 协同程序如何控制它向何处屈服?

Python 协同程序如何控制它向何处屈服?,python,coroutine,Python,Coroutine,生成器与函数/子例程的不同之处在于,如果反复调用,生成器可以从上次返回的行恢复,而不是从顶部重新启动 虽然生成器总是将控制权交还给它们的调用例程,*协同例程的不同之处在于它们能够控制将执行转移到何处 这是如何工作的,它与子例程调用有什么不同(子例程调用也会暂时挂起当前子例程的执行,同时控制立即传递执行的位置) *“所有这些使得生成器的功能与协同程序非常相似;它们多次屈服,有多个入口点,可以暂停执行唯一的区别是生成器函数无法控制在其产生后执行应在何处继续;控件总是传递给生成器的调用者。”——Py

生成器与函数/子例程的不同之处在于,如果反复调用,生成器可以从上次返回的行恢复,而不是从顶部重新启动

虽然生成器总是将控制权交还给它们的调用例程,*协同例程的不同之处在于它们能够控制将执行转移到何处

这是如何工作的,它与子例程调用有什么不同(子例程调用也会暂时挂起当前子例程的执行,同时控制立即传递执行的位置)


*“所有这些使得生成器的功能与协同程序非常相似;它们多次屈服,有多个入口点,可以暂停执行唯一的区别是生成器函数无法控制在其产生后执行应在何处继续;控件总是传递给生成器的调用者。”——Python手册,v2.5-3.7(自从引入了yield表达式以来)


此外,康威(1963年)提出的协同程序涉及使用对称语法相互传递控制,类似于对输入/输出函数的调用。(类似地,《计算机编程艺术》第3版给出了一个国际象棋游戏中每个玩家的协同程序示例,描述了“无法区分哪一个是另一个的子例程“因为两个协同例程都像子例程一样调用另一个。)

这里的混淆部分在于术语“协同例程”“通常应用得很松散。最初对协同路由的描述暗示了一组代码块,这些代码块可以对称地、有选择地转移控制权,以便从停止的地方彼此恢复。尽管如此,它仍然只是一个低层次的结构(在一个像子程序调用堆栈这样的概念仍在开发的时代),并留下了一些未指明的细节。特别是,如果协同路由能够挂起和恢复,甚至可以从嵌套的子例程调用中恢复,则需要为每个调用管理单独的内存堆栈(以避免碎片),因此它们将构成协作多任务(也称为光纤或轻量级/微型/绿色/用户线程,在单个抢占式操作系统线程内实现一般并发)。或者,如果协同路由仅从其顶层(而非嵌套例程)相互传递控制,则它们都可以共享现有堆栈

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

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

此外,“协同路由”可以协调将其所有事件循环通信委托给嵌套的子“协同路由”,直到该子“协同路由”终止。这类似于子例程调用的协同路由。在python中,
wait
yield from
是该委托的语法糖

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

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

协同程序是否控制它们向何处屈服?

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

如果只有两个例程同时执行(例如,一个生成器加上对其进行迭代的例程),那么这个问题无论如何都是没有意义的。协同例程的原始用例是并发运行数据处理管道的连续阶段,即通过嵌套的生成器进行流式处理(与多过程算法相比,最大限度地减少了延迟和临时存储);对于这种链式拓扑,嵌套python生成器的非对称语法确实允许选择下一步恢复哪个例程

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

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

调用和生成都是挂起当前例程的方法

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

涉及