http://blog.csdn.net/wuhenyouyuyouyu/article/details/52709475
http://www.ccidnet.com/2005/0722/292705.shtml
出处:http://simple-is-better.com/news/697
Continuation表示一个运算在其指定位置的剩余部分。当Continuation作为语言的第一类(First-class)对象时,可用于实现多种控制结构。同样作为控制结构,First-class continuation的表达力比协程更加强大,而且有着明确定义的语义,以至于在它出现之后对协程的研究就几乎完全停止了。但后来Revisiting Coroutines中证明了完全协程与One-shot continuation的表达力是完全相同的,而且协程更容易理解和使用,在某些情况下也更加高效。
理解Continuation
Continuation是一种描述程序的控制状态的抽象,它用一个数据结构来表示一个执行到指定位置的计算过程;这个数据结构可以由程序语言访问而不是隐藏在运行时环境中。Continuation在生成之后可作为控制结构使用,在调用时会从它所表示的控制点处恢复执行。
注意Continuation所保存的控制状态中并不包括数据。关于这一点有个很有趣的“Continuation三明治”的描述:
假设你在厨房里的冰箱前面,正打算做一个三明治。这时你获取一个Continuation放进兜里。然后从冰箱拿了些火鸡和面包做了个三明治,放在了桌子上。
现在调用兜里的那个Continuation,你会发现你又站在了冰箱前,正打算做个三明治。
但这时桌子上已经有个三明治了,而且火鸡和面包也不见了。于是你吃掉了三明治。
Continuation以及与相关的call/cc(Call-with-current-continuation)、CPS(Continuation-passing style)这几个概念比较难理解也容易混淆,要彻底把它们搞明白还真得花一番功夫。Continuation的资料也不多,这里列出几篇中文资料供参考:
- 从Continuation概念说到它在开发中的应用,这篇文章对Continuation的实现和CPS讲解得非常清楚。
- Continuations Made Simple and Illustrated,早年Python社区的讨论,有中文翻译简介延续“Continuation”。
- 尾递归与Continuation,老赵(@jeffz_cn)在C#中使用CPS的演示。
所谓continuation,其实本来是一个函数调用机制。我们熟悉的函数调用方法都是使用堆栈,采用Activation record或者叫Stack frame来记录从最顶层函数到当前函数的所有context。一个frame/record就是一个函数的局部上下文信息,包括所有的局部变量的值和SP,PC指针的值(通过静态分析,某些局部变量的信息是不必保存的,特殊的如尾调用的情况则不需要任何stack frame。
不过,逻辑上,我们认为所有信息都被保存了)。函数的调用前往往伴随着一些push来保存context信息,函数退出时则是取消当前的record/frame,恢复上一个调用者的record/frame。
像pascal这样的支持嵌套函数的,则需要一个额外的指针来保存父函数的frame地址。不过,无论如何,在任何时候,系统保存的就是一个后入先出的堆栈,一个函数一旦退出,它的frame就被删除了。
Continuation则是另一种函数调用方式。它不采用堆栈来保存上下文,而是把这些信息保存在continuation record中。这些continuation record和堆栈的activation record的区别在于,它不采用后入先出的线性方式,所有record被组成一棵树(或者图),从一个函数调用另一个函数就等于给当前节点生成一个子节点,然后把系统寄存器移动到这个子节点。
一个函数的退出等于从当前节点退回到父节点。这些节点的删除是由garbage collection来管理。如果没有引用这个record,则它就是可以被删除的。
这样的调用方式和堆栈方式相比的好处在哪里呢?最大的好处就是,它可以让你从任意一个节点跳到另一个节点。而不必遵循堆栈方式的一层一层的return方式。比如说,在当前的函数内,你只要有一个其它函数的节点信息,完全可以选择return到那个函数,而不是循规蹈矩地返回到自己的调用者。
你也可以在一个函数的任何位置储存自己的上下文信息,然后,在以后某个适当的时刻,从其它的任何一个函数里面返回到自己现在的位置。
Scheme语言有一个CallCC (call with current continuation)的机制,也就是说:取得当前的continuation,传递给要call的这个函数,这个函数可以选择在适当的时候直接return到当前的continuation。
经典的应用有:exception,back-tracking算法,coroutine等。应用continuation对付exception是很明显的,只要给可能抛出异常的函数一个外面try的地方的continuation record,这个函数就可以在需要的时候直接返回到try语句的地方。
Exception-handling也可以利用continuation。c++等语言普遍采用的是遇到exception就直接中止当前函数的策略,但是,还有一种策略是允许resume,也就是说,出现了异常之后,有可能异常处理模块修复了错误发生的地方然后选择恢复执行被异常中断了的代码。
被异常中断的代码可以取得当前的continuation,传递给异常处理模块,这样当resume的时候可以直接跳到出现异常的地方。
Back-tracking算法也可以用类似的方法,在某些地方保存当前的continuation,然后以后就可以从其它的函数跳回当前的语句。
Coroutine是一个并行计算的模型。它让两个进程可以交替执行。典型的coroutine的应用例子如consumer-producer。比如说:
Producer ( c ):
Loop:
Produce;
CallCC c
Consumer( p ):
Loop:
Consume;
Callcc p
|
这两个线程接受对方的continuation,在需要交出控制的时候,就把自己的continuation传递给对方。如此,两者就可以交替执行,而不需要返回。 Continuation机制的优化始终不是一个trivial的问题,实际上采取continuation的语言不多。而且,continuation调用方式依赖垃圾收集,也不是c/c++这类中低级的语言所愿意采用的。 不过,continuation的思想仍然是有其用武之地的。有一种设计的风格叫做continuation-passing-style。它的基本思想是:当需要返回某些数据的时候,不是直接把它当作函数的返回值,而是接受一个叫做continuation的参数,这个参数就是一个call-back函数, 它接受这个数据,并做需要做的事情。 举个例子:
X = f(); Print x; |
把它变成continuation-passing-style, 则是:
f(print); |
F()函数不再返回x,而是接受一个函数,然后把本来要返回的x传递给这个函数。这个例子也许看上去有点莫名其妙:为什么这么做呢?对Haskell这样的语言,一个原因是:当函数根据不同的输入可能返回不同类型的值时,用返回值的话就必须设计一个额外的数据结构来处理这种不同的可能性。 比如:一个函数f(int)的返回值可能是一个int,两个float或者三个complex,那么,我们可以这样设计我们的函数f.
F:: Int -> (Int->a) -> (Float->Float->a) -> (Complex->Complex->Complex->a) -> a |
这个函数接受一个整形参数,三个continuation回调用来处理三种不同的返回情况,最后返回这三个回调所返回的类型。 另一个原因:对模拟imperative风格的monad,可以在函数中间迅速返回(类似于C里面的return或者throw)。 对于C++,我想,除了处理不同返回类型的问题,另一个应用可以是避免返回值的不必要拷贝。虽然c++现在有NRV优化,但是这个优化本身就很含混,各个编译器对NRV的实现也不同。C++中的拷贝构造很多时候是具有副作用的,作为程序员,不知道自己写的的副作用到底是否被执行了,被执行了几次,总不是一个舒服事。 而continuation-passing-style,不依赖于任何偏僻的语言特性,也不会引入任何的模棱两可,也许可以作为一个设计时的选择。举个例子,对于字符串的拼接,如果使用continuation-passing-style如下:
Template<class F>
Void concat(const string& s1,const string& s2, F ret)
{
String s(s1);
S.append(s2);
ret(s);//此处,
本来应该是return(s),
但是我们把它变成ret(s)。
}
|
我们就可以很安心地说,我们此处没有引入任何不必要的拷贝,不论什么编译器。当然,continuation style的问题是,它不如直接返回值直观,类型系统也无法保证你确实调用了ret(s)。而且,它需要一个function object,c++又不支持lamda,定义很多trivial的functor也会让程序变得很难看。利弊如何,还要自己权衡。 (T117)
First-class continuation
Continuation这个术语很多时候也用于表示First-class continuation。这时它表示一种语言构造,使语言可以在任意点保存执行状态并且在之后的某个点返回。这里与协程做比较的正是First-class continuation(或者说是call/cc机制),而不是Continuation结构或CPS编程风格。
调用call/cc时,会把当前Continuation打包成一个第一类对象。然后这个捕获的Continuation被传给call/cc的参数——此参数必须是带一个参数的过程。如果在这个过程中没有调用Continuation就返回了,则返回值作为call/cc的值。如果在其中调用了Continuation并传给它一个值,则这个值立即返回到call/cc处。
基于First-class continuation很容易实现完全协程,主要思路是在切换协程时保存当前Continuation并调用目标协程的Continuation。以下是由Marc De Scheemaecker基于Ruby call/cc实现的完全对称协程:
class Coroutine
def initialize(&block)
# Creates a coroutine. The associated block does not run yet.
@started = false
@finished = false
@block = Proc::new {
callcc{|@cc|}
block.call
@finished = true
@started = false
}
enddef start
# Starts the block. It's an error to call this method on a coroutine
# that has already been started.
raise "Block already started" if @started@started = true
@block.call
enddef switch(coroutine)
# Switches context to another coroutine. You need to call this method
# on the current coroutine.
switch = trueif not coroutine.finished? then
callcc{|@cc|}if switch then
switch = falseif coroutine.running? then
coroutine.continuation.call
else
coroutine.start
end
end
end
enddef running?
# Returns true if the associated block is started and has not yet
# finished
@started and not @finished
enddef finished?
# Returns true if the associated block is finished
@finished
enddef continuation
# Returns the associated continuation object
@cc
endend
One-shot continuation
所谓One-shot continuation,即只允许调用一次的Continuation。标准的Continuation是允许多次调用(Multi-shot)的,但是很难高效地实现这样的Continuation,因为每次调用之前都必然要生成一个副本,而且在绝大多数情况下Continuation都只会被调用一次,受此启发Bruggeman et al.提出了One-shot continuation的概念和控制符call/1cc。One-shot continuation几乎可以所有应用中替换标准Continuation(包括上面协程的实现)。多次调用One-shot continuation会引发错误,无论是隐式调用(从传给call/1cc的过程中返回)还是显式调用(直接调用由call/1cc创建的Continuation)。
前面提到过完全协程与One-shot continuation的表达力是相同的,证明方式便是它们可以相互实现。从对称协程的视角来看,捕获一个One-shot continuation相当于新建一个协程并把控制传递给它。调用时相当于把控制返回给创建者。这种相似性使得基于对称协程可以很简洁地实现call/1cc。
下面是Lua实现One-shot continuation的代码,首先实现一个完全对称协程:
coro = {}
coro.main = function() end
coro.current = coro.main-- function to create a new coroutine
function coro.create(f)
local co = function(val)
f(val)
error("coroutine ended")
end
return coroutine.wrap(co)
end-- function to transfer control to a coroutine
function coro.transfer(co, val)
if coro.current == coro.main then
return coroutine.yield(co, val)
end-- dispatching loop
while true do
coro.current = co
if co == coro.main then
return val
end
co, val = co(val)
end
end
然后是call1cc:
function call1cc(f)
-- save the continuation "creator"
local ccoro = coro.current
-- invoking the continuation transfers control
-- back to its creator
local cont = function(val)
if ccoro == nil then
error("one shot continuation called twice")
end
coro.transfer(ccoro, val)
end
-- when a continuation is captured,
-- a new coroutine is created and dispatched
local val
val = coro.transfer(coro.create(function()
local v = f(cont)
cont(v)
end))
-- when control is transfered back, the continuation
-- was "shot" and must be invalidated
ccoro = nil
-- the value passed to the continuation
-- is the return value of call1/cc
return val
end
效率问题
可以看到,Continuation可以实现协程,同时协程也可以实现One-shot continuation,但这两种相反实现的效率并不相同。
在Bruggeman et al.描述的One-shot continuation实现中,控制栈由栈段(Stack segment)组成的链表表示,整个控制栈被构造成栈帧(Stack of frame)或活动记录(Activation record)。捕获Continuation时,当前栈段被保存到Continuation中,然后分配一个新的栈段。调用Continuation时,丢弃当前栈段,控制返回到之前保存的栈段。
创建一个协程同样包括分配一个单独的栈,但挂起和恢复协程的代价只比标准的函数调用略高。
使用协程实现One-shot continuation时,创建一个单独的协程——即栈“段”——就足以表示一个Continuation。因此,通过协程实现的One-shot continuation与直接实现效率几乎相同。而以Continuation实现协程时,通常每次协程挂起时都需要捕获一个新的Continuation。这导致每次控制转换都需要重新分配一个栈段,相比直接实现的协程效率要大大降低并且需要更多内存。
这里有一篇Lua、LuaJIT Coroutine和Ruby Fiber的切换效率对比,我猜测大概就是因为Ruby是在call/cc之上实现的Fiber。
浙公网安备 33010602011771号