协程(三)协程与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的演示。
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。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。