Actor、 Coroutine和 Continuation这三个概念由于并发的受关注而被经常提到,这里主要想谈下这三者的区别和联系,以便更好的区分问题领域和讨论。首先,Actor和Coroutine在我看来是两种并发模型,仅针对于并发这个领域,而Continuation则是程序设计领域的一个概念,相比于Actor和Coroutine是一个更基础的概念。
那么,什么是Continuation?这个要从表达式的求值说起。一个表达式的求值可以分为两个阶段:“ What to evaluate?”和“ What to do with the value”,“What to do with the value”就是计算的Continuation。以下面这段代码为例:
if
x
<
3
then
return x + 1
else
return x
end
return x + 1
else
return x
end
考察其中的表达式x<3,这个表达式就是“what to evaluate?”,代表你将计算的东西,然后根据x<3的结果决定是执行x+1还是直接返回x,这个根据x<3的值来决定下一步的过程就是这个表达式的Continuation,也就是"what to do with the value"。怎么得到某个表达式的Continuation呢?在支持Continuation的语言里提供了call-with-current-continuation的函数,通常简称为call/cc,使用这个函数你就可以在任何代码中得到Continuation。进一步,continuation有什么作用呢?它可以做的事情不少,如nonlocal exits、回溯、多任务的实现等等。例如在scheme中没有break语句,就可以用call/cc实现一些此类高级的控制结构:
(call
/
cc (
lambda
(
break
)
( for - each ( lambda (x) ( if ( < x 0) ( break x)))
' (99 88 -77 66 55))
# t))
( for - each ( lambda (x) ( if ( < x 0) ( break x)))
' (99 88 -77 66 55))
# t))
上面这段代码查找列表(99 88 -77 66 55)中的负数,当查找到的时候马上从迭代中退出并返回该值,其中的break就是一个continuation。刚才还提到continuation可以实现回溯,那么就可以实现一个穷举的机器出来用来搜索解空间,也就是类似Prolog中的回溯机制,在SICP这本书里就介绍了如何用call/cc实现一个简单的逻辑语言系统。更著名的就是神奇的amb操作符,有兴趣可以看看 这里。
接下来我们来看看如何continuation实现多任务,在Continuation的 维基百科里给了一段代码来展示如何用scheme来实现coroutine,我稍微修改了下并添加了注释:
;;continuation栈,保存了等待执行的continuation
(define call/cc call-with-current-continuation)
(define *queue* '())
(define (empty-queue?)
(null? *queue*))
(define (enqueue x)
(set! *queue* (append *queue* (list x))))
(define (dequeue)
(let ((x (car *queue*)))
(set! *queue* (cdr *queue*))
x))
;;启动协程
(define (resume proc)
(call/cc
(lambda (k)
;;保存当前continuation,执行proc
(enqueue k)
(proc))))
;;让出执行权
(define (yield)
(call/cc
(lambda (k)
;;保存当前continuation,弹出上一次执行的cont并执行
(enqueue k)
((dequeue)))))
;;停止当前协程或者当没有一个协程时停止整个程序,最简单的调度程序
(define (thread-exit)
(if (empty-queue?)
(exit)
((dequeue))))
(注:scheme以分号开头作为注释)
(define call/cc call-with-current-continuation)
(define *queue* '())
(define (empty-queue?)
(null? *queue*))
(define (enqueue x)
(set! *queue* (append *queue* (list x))))
(define (dequeue)
(let ((x (car *queue*)))
(set! *queue* (cdr *queue*))
x))
;;启动协程
(define (resume proc)
(call/cc
(lambda (k)
;;保存当前continuation,执行proc
(enqueue k)
(proc))))
;;让出执行权
(define (yield)
(call/cc
(lambda (k)
;;保存当前continuation,弹出上一次执行的cont并执行
(enqueue k)
((dequeue)))))
;;停止当前协程或者当没有一个协程时停止整个程序,最简单的调度程序
(define (thread-exit)
(if (empty-queue?)
(exit)
((dequeue))))
这其实就是一个coroutine的简单实现,context的保存、任务的调度、resume/yield原语……样样俱全。使用起来类似这样,下面这段程序轮流打印字符串:
(define (display-str str)
(lambda()
(let loop()
(display str)
(newline)
(yield)
(loop))))
;;;创建两个协程并启动调度器
(resume (display-str "This is AAA"))
(resume (display-str "Hello from BBB"))
(thread-exit)
(lambda()
(let loop()
(display str)
(newline)
(yield)
(loop))))
;;;创建两个协程并启动调度器
(resume (display-str "This is AAA"))
(resume (display-str "Hello from BBB"))
(thread-exit)
任务非常简单,打印下传入的字符串并换行,然后让出执行权给另一个任务执行,因此输出:
This is AAA
Hello from BBB
This is AAA
Hello from BBB
This is AAA
Hello from BBB
This is AAA
Hello from BBB
……
Hello from BBB
This is AAA
Hello from BBB
This is AAA
Hello from BBB
This is AAA
Hello from BBB
……
谈了这么多continuation的应用,事实上我想说明的是continuation可以用来实现协程,Ruby 1.9中call/cc和Fiber的实现(在cont.c)大体是一样的同样说明了这一点。
接下来我们讨论下Actor和Coroutine的关系,上面提到Actor是一种并发模型,我更愿意称之为一种编程风格,Actor跟message passing、Duck Typing是一脉相承的。Actor风格是可以这么描述:将物理世界抽象成一个一个的Actor,Actor之间通过发送消息相互通信,Actor不关心消息是否能被接收或者能否投递到,它只是简单地投递消息给其他actor,然后等待应答。Actor相比于Coroutine是一种更高层次的抽象,它提供的receive和pattern match的原语更接近于现实世界,而使用coroutine编程你还需要手工介入任务调度,这在Actor中是由一个调度器负责的。
同样,Actor可以用coroutine实现,例如Ruby有个 revactor项目,就是利用1.9引入的Fiber实现actor风格的编程,它的实现非常简单,有兴趣地可以看看,其实跟continuation实现coroutine类似。但是Actor并不是一定要用coroutine才能实现,Actor是一种编程风格,你在Java、C#、C++中同样可以模拟这样的方式去做并发编程,.net社区的老赵 实现过一个简单的Actor, Scala的Actor实现是基于外部库,利用scala强大的元编程能力使得库的使用像内置于语言。
总结下我想表达的:Continuation是程序设计领域的基础概念,它可以用于实现coroutine式的多任务,Actor是一种比之coroutine更为抽象的编程风格,Actor可以基于Coroutine实现但并非必须,Actor和Coroutine都是现在比较受关注的并发模型。