统一了线程和事件的Actors(Actors That Unify Threads and Events)(第一节)
[email protected]
工作中使用了一年的Scala,跟着我们老大
Deng Caoyuan先生学习并做了不少分布式实时处理方面的工作,现在越来越觉得Scala中的Actor模型真是个好东西。这几天研读了Philipp Haller和Martin Odersky关于Actors设计的论文,很受启发,借此机会将论文翻译下来,与大家共勉,如果有翻译错误或者不精确的地方,敬请谅解并赐教。
本文翻译自
http://lamp.epfl.ch/~phaller/doc/haller07actorsunify.pdf,原作者 Philipp Haller 和 Martin Odersky.
摘要:在基于消息传输解决并发的方案和诸如JVM的虚拟机之间有不和谐之处,比如虚拟机经常将自己的线程同重量级的操作系统的进程对应起来。在没有轻量级的进程抽象情况下,用户经常被迫以事件驱动的方式写一些并发处理的程序,而这些基于事件驱动的程序逻辑处理使得业务流程晦涩难懂,从而增加了程序员的负担。
在这篇论文中我们展示了如何在使用了单独的Actor抽象之后将基于线程的编程模型和基于事件的编程模型统一起来。在使用了Scala编程语言中的高级抽象机制之后,我们在未修改的JVM之上实现了我们自己的线程和事件的统一。并且我们的编程模型与底层JVM的线程模型集成的非常好。
第一节. 介绍
最近并发问题由于两个趋同的趋势而受到了极大的关注:首先,多核处理器使得并发成为高效的程序执行的基础要素;其次,分布式计算和web服务本质上也是并发处理。因为基于事件的并发处理也许是一种同时解决这两种挑战的方案而颇具吸引力。这种方案被看作是线程的一种高级模型,这种模型具有将分布计算通用化的潜力。许多实际应用中的消息传递系统都是Actor模型的实例,比如Erlang编程语言就是这种并发处理机制的一种实现。Erlang支持大规模的并发处理,比如他非常轻量的实现了电话交换这种并发处理。
在像JVM这样一种主流的平台上却缺少一种相应的具有吸引力的并发实现。JVM平台上标准的并发由共享内存的线程锁机制组成,这种机制通过耗费大内存和线程切换上下文的额外开销为代价实现了并发。在这样的平台上实现的事件驱动模型,都是基于独立计算的交叉存储方式(guibin注:the interleaving of independent computations)。因此在这样的平台上以显示的事件驱动的风格编程,既复杂又容易出错,因为他涉及到了控制的倒置(guibin注:inversion of control,请参考“Why events are a bad idea (for high-concurrency servers)”,von Behren, J.R., Condit, J., Brewer, E.A.)。
在之前的文章中(guibin注:之前的文章是Event-based Programming without Inversion of Control,作者Haller, P., Odersky, M.)我们开发了基于事件的actors,这种actors能够使人们编写事件驱动的系统而不使控制倒置(guibin注:原文without inversion of control)。除了基于事件的actors的receive操作不能正常的返回到调用它的线程中之外,基于事件的actors与基于线程的actors均支持相同的其他操作,因此这种actor的完全连续性(guibin注:原文the entire continuation of such an actor)也必须是receive操作的一部分。这就使得通过连续闭包(guibin注:原文continuation closure)对挂起的actor建模成为可能,这种建模方式处理挂起的actor要远比挂起一个真正的线程要廉价的多。
在这篇论文中我将展示一个基于线程的和基于事件的acotrs的统一体。actor能够通过完全的堆栈帧被挂起(receive方式)(guibin注:原文 suspend with a full stack frame)或者通过连续闭包挂起(guibin注: 原文 suspend with just a continuation closure)(react方式)。第一种方式的挂起对应于线程的挂起,第二种方式的挂起是基于事件的编程(guibin注:挂起)。新系统合并了这两种挂起模型的优势。线程支持阻塞操作,比如像系统I/O,并且还能够在多核处理器上并行执行;基于事件的计算,另一方面,会更加的轻量级,并且能够轻易的扩展到相当大数量的actors同时执行。我们也展示了一系列的能够灵活组合这些actors的combinator。
这篇论文在几个方面改善了我们之前的工作。首先,actor是否是与线程无关一直被等到运行的时刻才被决定。一个actor也许会多次丢弃与它相对应的底层的线程栈。其次,我们之前的工作没有涉及到对两种actors的组合。之前既没有一个方案能够解决基于事件的actor的序列组合,也没有一个方法能够解决在同一个程序中对基于线程的actors和基于事件的actors的组合。
本论文中提出的方案已经在Scala的actors库中实现了。这些实现既不需要特殊的语法,也不许要编译器支持。基于函数库的实现能够根据新的需求灵活扩展的优点。事实上actors库中的实现是之前多次迭代开发的结果。然而为了方便使用,actors库利用了Scala的几个高级抽象能力,比如偏函数(guibin注:原文partial function)和模式匹配(guibin注: 原文pattern matching)。
截止目前,来自用户的经验表明,actors库在基于JVM的系统中进行并发编程要比其他技术容易的多,它简化了并发编程的复杂性,这些简化源自以下几个因素:
- 既然从设计的角度看访问一个actor的邮箱(guibin注:原文mailbox)是非竞争的,那么基于消息的并发本身要比基于内存共享的锁机制实现的并发要安全。我们相信基于模式匹配而实现的消息匹配在很多情况下会更方便。
- Actors是轻量级的。一个支持5000个并发活跃的线程的虚拟机上能够支持1,200,000个同时活跃的actors。用户因此从线程池中解放出来了。
- Actors同正常的虚拟机中的线程是完全互操作的。每一个虚拟机中的线程都被当成一个actor。这就使得actors的通信和监控能力比正常的虚拟机中的线程要高级得多。
其他人做过的解决并发问题的相关工作。Lauer 和 Needham也注意到线程和事件都具有两面性。他们建议选择任何一种方式都得基于底层的平台。在大概20年前,Ousterhout争论说使用多线程是个不好的想法,不仅因为他们经常执行的效率很糟糕,而且因为他们很难使用。最近,Behren及其他人指出,尽管事件驱动的程序比相同的基于线程的程序性能表现要好,但是它太难使用了。这主要因为两个原因:首先,程序的交互逻辑分散在多个事件处理器中(guibin注:原文event handler)(或者是分散在多个class中,比如状态设计模式),其次在各处理器中的控制流都比较晦涩,因为这些控制流都是通过操纵共享状态而实现的。在Capriccio系统中为了要达到相同的效果,它使用静态分析和编译技术来将多线程程序转换成相互协调工作的事件驱动的程序。
还有几种其他的方法可以避免上述的控制倒置问题。但是这些方法不是缺少扩展性,就是缺少对堵塞的支持, Termite Scheme将Erlang的编程模型集成进Scheme中,Scheme的第一个class continuations被开发出来用来表述进程移植(gubin注:原文 Scheme’s first-class continuations are exploited to express process migration.)然而他们的系统看起来并不支持多核处理器。而且所有已发布的该系统的测试基准都跑在单核处理器上。Responders提供了类似java扩展的一种事件循环抽象(guibin注:原文event-loop abstraction)。他们的实现是使用为每一个事件循环使用一个虚拟机线程,因此扩展性就局限在标准的JVM上了。SALSA是一种基于java的actor语言,这种语言也有相似的局限性(每一个actor都只能运行在自己的线程上)。另外消息传递的性能也因为额外开销太多而损失较大,这些损失产生是由于它将消息传递机制反射到基本的函数调用。Timber也是一种专为实时嵌入式系统而设计的面向对象的函数式语言,它提供了在可反应对象(guibin注:原文reactive objects)直接原始的同步和异步的消息通信,与我们的编程模型相比,可反应对象不可以调用可能产生不确定阻塞的操作。Frugal objects((FROBs))就是分布式的可反应对象,这些对象能够同有类型的事件交互。FROBs就是具有基于事件计算模型的一种基础actors。与Timber种的可反应对象相对应,FROBs不可以调用阻塞操作。
Li 和 Zdancewic提议了一种基于语言的方法统一事件和线程,通过把事件集成进语言级别的线程,他们获得了很可观的性能提升。然而,阻塞式的系统调用又必须被包装进非阻塞的操作中,此外如果需要给线程库添加新的事件源(比如注册事件处理器,添加事件循环等),就需要进行侵入式的改动。
actor模型已经被集成进入到不同的Smalltalk系统中,Actalk就是为Smalltalk-80而写的actor库,这个库同样也不支持多核处理器。Actra扩展了 Smalltalk/V虚拟机,并提供了重量级的处理。相反我们在未修改的虚拟机之上实现了轻量级的actors。
在第7节,我们展示了我们自己的actor实现,它能轻松扩展到是诸如SALSA所支持的纯粹基于线程的系统的两个数量级以上的actors。除此之外,我们的模型在系统中能够随着CPU核数目的增加而随之扩展,我们统一的actor模型提供了对阻塞操作的无缝支持,因此,已经存在的那些基于线程的API不用包装成非阻塞操作。不像类似Actra这样的方法,我们的实现提供了在未修改的虚拟机之上的轻量级的actor抽象。
我们的函数库受到Erlang的优雅的编程模型很大的影响。Erlang是一种动态类型的函数式编程语言,这种语言专门是为实时控制系统设计的。轻量级的被隔离的进程,使用模式匹配的异步消息传递,和控制错误传播都已经被证实是非常有效的。我们主要的贡献之一就在于将Erlang的编程模型集成进完全面向对象的函数式编程语言中。除此之外,通过将编译器的逻辑放入函数库,使得Scala语言与标准和未修改的JVM的兼容。相对于Erlang的编程模型,我们添加了组合的新形式,比如通道(guibin注:原文channel),通道提供了强类型支持和安全的交互通信。
使用continuations实现轻量级的并发处理的想法已经说了好几次了,但是现有的技术没有适合诸如JVM的虚拟机的,因为(1)访问运行时堆栈限制太严格,(2)基于堆的堆栈打破了同现有代码的互操作性。然而在Mach 3.0 kernel中实现的线程管理的方法是最后的同我们的概念相似的实现方式。当一个线程在内核中被阻塞时,它既可以保留自己的寄存器状态和堆栈以便稍后能够恢复它的状态;或者也可以保留一个指针指向continuation函数。除了函数指针,我们也可以使用闭包自动的将引用的堆栈变量移动到堆上,从而在很多情况下避免显示的状态管理。
比如关于建立快速的web服务这样一项工作(guibin注:原文There is a rich body of work on building fast web servers),是使用事件机制实现,还是使用事件和线程的结合机制(比如SEDA)来实现,已经超出了本论文讨论的范围。
我们对于基于actor的高层次的编程模型的整合,提供了强有力的不变的和轻量级的并发处理,这种使用了已经存在的现有的主流虚拟机平台的整合对于目前已知的技术来讲是唯一的方式。我们相信我们的处理方法对于在多核系统上开发并发处理的软件具有质的提升。
本章剩余部分是按照如下方式组织:下一节我们介绍我们的编程模型并且解释在目前的函数库中如何实现的。在第三节中我们介绍一个大的例子,并且这个例子在随后的章节中还会再次见到。我们的统一编程模型将在第四节中介绍。第五节介绍了作为actors通用化的通道(gubin注:原文channel)。第六节我们通过一个实例学习我们的统一编程模型如何应用在高级web应用中。一些试验性的结果在第七节中介绍,第八节是结论。
2011-03-16
Guibin