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
考察其中的表达式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))
上面这段代码查找列表(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以分号开头作为注释)
这其实就是一个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)
任务非常简单,打印下传入的字符串并换行,然后让出执行权给另一个任务执行,因此输出:
This is AAA
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都是现在比较受关注的并发模型。