协程 vs. 续体 vs. 生成器

167

协程、续延和生成器有何不同?


3
协程和续延是否有效等效?我知道可以使用续延来模拟协程,但是能否使用协程来模拟续延,因为续延严格来说更加强大?请注意不要改变原意。 - nalply
3个回答

141

我将从生成器开始讲起,因为它们是最简单的情况。正如 @zvolkov 提到的那样,它们是可以重复调用但不返回值的函数/对象,但在调用时会返回(yield)一个值并暂停执行。当它们再次被调用时,它们将从上次挂起执行的位置开始,并再次执行。

生成器本质上是精简版的(非对称)协程。协程和生成器的区别在于,协程可以在初始调用之后接受参数,而生成器则不能。

有点难以想出一个使用协程的简单示例,但这是我最好的尝试。拿这段(虚构的)Python代码作为例子。

def my_coroutine_body(*args):
    while True:
        # Do some funky stuff
        *args = yield value_im_returning
        # Do some more funky stuff

my_coro = make_coroutine(my_coroutine_body)

x = 0
while True:
   # The coroutine does some funky stuff to x, and returns a new value.
   x = my_coro(x)
   print x
协程常用于词法分析器和解析器中。如果编程语言中没有协程或者没有模拟协程的机制,那么词法分析和解析代码需要混在一起,尽管它们实际上是两个不同的关注点。但使用协程,你可以将词法分析和解析代码分离出来。
(下面我将略去对称协程和非对称协程之间的区别,简单地说它们是等效的,你可以从一个转换到另一个,而非对称协程——最像生成器的协程——更容易理解。我将概述如何在Python中实现非对称协程。)
续延其实非常简单。它们只是代表程序中另一个点的函数,如果你调用它,执行会自动切换到该函数所代表的点。你每天都在使用它们的非常受限版本,甚至无法意识到它们的存在。比如,异常可以被看作是某种内部续延。我将给你提供一个基于Python的伪代码示例来说明续延。
假设Python有一个名为“callcc()”的函数,它接受两个参数,第一个是函数,第二个是要调用它的参数列表。这个函数的唯一限制就是它所接受的最后一个参数将是一个函数(这将是我们当前的续延)。
def foo(x, y, cc):
   cc(max(x, y))

biggest = callcc(foo, [23, 42])
print biggest

callcc() 会使用当前的延续(cc,也就是程序中调用 callcc() 的位置)调用 foo()。当 foo() 调用当前的延续时,实际上相当于告诉 callcc() 使用你当前调用当前延续的值返回,并且回滚栈到创建当前延续的位置,也就是调用 callcc() 的位置。

最终结果是,我们假设的 Python 变量将打印出 '42'


6
一个小问题:delimited continuation 是函数,但 undelimited continuation 不是:http://okmij.org/ftp/continuations/undelimited.html#delim-vs-undelim - Frank Shearar
2
这是一个很好的观点。话虽如此,在大多数实际应用中,当人们说“continuation”时,他们指的是部分/有限续集。引入其他各种类型的续集会使解释变得混乱。 - Keith Gaughan
4
首先,让我们明确一件事,我写下这篇文章已经有五年了。你来得有些晚了。其次,我知道无界延续不是函数,但你可以试着在不使用这个术语的情况下解释它们的工作原理,同时保持语言通俗易懂。从普通程序员的角度来看,无界延续不返回值只是一个单次函数,这与函数的定义不完全相符,但至少是可以理解的。 - Keith Gaughan
2
我并没有迟到参加派对,因为当我搜索“协程 vs 生成器”时,这是我在谷歌上得到的第一个结果。我希望能找到一些关于它们之间差异的好信息。无论如何,我在其他地方找到了它。而且我不是第一个指出你有关连续性的解释是错误的人。问题在于,有人会理解错误,并在以后遇到同样的单词用于不同的事物时可能感到困惑。 - Ivancho
1
@KeithGaughan 不,他只是太忙于试图证明自己比你聪明了... 他肯定不会编辑答案的 :D - El Ninja Trepador
显示剩余18条评论

34

Coroutine是几个过程之一,它们轮流执行任务,然后暂停以将控制权交给组中的其他协程。

Continuation是一个“指向函数”的指针,您可以将其传递给某些过程,在该过程完成时执行(“继续”)。

Generator(在.NET中)是一种语言构造,可以输出一个值,"暂停"方法的执行,然后在请求下一个值时从同一点继续。


我意识到答案可能不准确,但在这个问题的水平上,我尽量保持简单。此外,我自己也不是很理解这一切 :) - Andriy Volkov
Python中的生成器类似于C#版本,但它被实现为创建迭代器对象实例的特殊语法,该迭代器对象返回您提供的“函数”定义返回的值。 - Benson
2
一个小修正:“……包括调用堆栈和所有变量,但不包括它们的值”(或者只是省略“所有变量”)。Continuations 不会保留值,它们只包含调用堆栈。 - nalply
不,continuations(延续)不是“指向函数的指针”。在最简单的实现中,它包含一个指向函数的指针和一个环境来保存局部变量。除非使用类似于call/cc的东西来捕获它并返回值,否则它永远不会返回。 - NalaGinrut

13
在更新版本的Python中,您可以使用generator.send()向生成器发送值,这使得Python生成器有效地成为协程。
Python生成器和其他生成器(例如Greenlet)之间的主要区别在于,在Python中,您的yield value只能返回给调用者。而在Greenlet中,target.switch(value)可以将您带到特定的目标协程并产生一个值,其中target将继续运行。

4
但在Python中,所有yield调用必须在同一个函数中,该函数称为“生成器”。您无法从子函数中yield,这就是为什么Python被称为半协程,而Lua具有不对称协程的原因。(虽然有关于传播yield的提案,但我认为那只会让事情更加混乱。) - cdunn2001
9
Python3.3引入了"yield from"表达式,允许你从子生成器中使用yield。 - Linus Caldwell
@LinusCaldwell,“yield from”仍然写在生成器中,而不是在可能在生成器中调用的任意函数内。从这个意义上说,Python的协程仍然是无栈的。 - Gqqnbig

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接