Java的显式锁和同步模型刻下了岁月的痕迹。在最初设计Java语言时,它是一个奇妙的创新,但也埋下了祸根。Java并发模型本质上是面对两难境地时采取折中策略的产物。
锁太少,会导致并发代码不安全,出现竞态条件。锁太多,系统会丧失活力,代码瘫痪,工作毫无进展。这就是我们在第4章讨论过的,安全性与系统活力之间的矛盾。
使用基于锁的模型,必须照顾到给定时间内所有可能发生的并发操作。但随着程序变得越来越大,要做到滴水不漏会变得越来越困难。尽管Java有办法缓解一些问题,但核心问题还在,如果Java语言不能发布一个拒绝向后兼容的版本,就不可能从根本上解决这个问题。
非Java语言有机会从头开始。备选语言可以不向程序员暴露锁和线程的底层细节,而是在自己的运行时环境中提供额外的并发支持。
这应该没什么好奇怪的。毕竟在Java刚刚出现时,Java内存模型就受到过质疑。当时很多C和C++开发人员都对这种想法感到诧异,怎么能由运行时负责管理内存,而让开发人员远离这些细节呢?
我们来看一下Scala基于actor技术的并发模型,看它如何让并发编程变了样(也更简单)。
9.6.1 代码大舞台
actor是扩展scala.actors.Actor
,并实现了act
方法的对象。希望这个定义能跟你脑海中对Java线程的定义相呼应。它们最大的差别就是actor在大多数情况下都不会通过共享的数据进行沟通。
程序员在共享数据时必须采用最佳实践。如果你想在actor间共享状态,Scala不会阻止你。我们只是认为这么做不好。actor有沟通的渠道:mailbox,从另一个上下文中发送过来的消息(工作项)可以放在mailbox中交给actor,请参见图9-5。
图9-5 scala的actor和mailbox
要创建actor,扩展Actor
类就行:
import scala.actors._class MyActor extends Actor { def act { ... }}
这看起来跟Java代码中声明Thread
的子类很像。跟线程一样,我们也要告诉actor开始启动,并进入消息接收的状态,这要调用start
方法。
Scala同样提供了创建actor的工厂方法actor
(与Java里创建Runnable
匿名实现类的静态工厂方法相对应)。用它写出来的Scala代码很精炼:
val myactor = actor { ...}
传给actor
的代码块会变成act
方法中的内容。另外,这样创建的actor不需要再单独调用start
,它会自动启动。
这是一块香甜的语法糖,但我们还要介绍Scala并发模型的核心部件mailbox,所以别回味了,现在就去看看吧。
9.6.2 用mailbox跟actor通信
从另一个对象给actor发消息很简单,只要在actor对象上调用!
方法就行了。
然而在接收端要有代码处理这些消息,否则它们就会堆在mailbox里。另外,actor方法体通常需要有个循环,以便能处理所有流入的消息。我们在Scala REPL中实际操练一下:
scala> import scala.actors.Actor._ val myact = actor { while (true) { receive { case incoming => println(/"I got mail: /"+ incoming) } } }myact: scala.actors.Actor = [email protected]scala> myact ! /"Hello!/"I got mail: Hello!scala> myact ! /"Goodbye!/"I got mail: Goodbye!scala> myact ! 34I got mail: 34
上面代码中的receive
方法就是actor对消息的处理。而工厂方法的参数(代码块)则是消息处理方法的主体。
注意 总体来说,Scala模型跟我们第4章(代码清单4-13)讨论的处理模式相似,Java处理线程相当于acctor的角色,
LinkedBlockingQueue
相当于Scala中的mailbox。Scala只是以非常直白的方式为这种模式提供了语言和类库层面的支持,可以大量减少使用这种模式时所要编写的套路化代码。
尽管这个例子非常简单,但也包含了很多使用actor的基础知识:
在actor方法中要用循环的方式处理接收消息流;
用
receive
方法处理接收到的消息;用一组
case
作为receive
的主体。
最后这点还得继续讨论。这一组case
被称为偏函数1。之所以要这样用,是因为Scala中的actor还有一点比Java方便。具体来说就是mailbox是不区分类型的。也就是说你可以向actor发送任何类型的消息,actor可以用 类型化模式和构造器模式接收不同类型的消息。
1 在Scala中,偏函数是指类型为PartialFunction[-A,+B]
的函数。A
是其接受的函数类型,B
是其返回的结果类型。偏函数最大的特点就是它只接受其参数定义域的一个子集,而对于这个子集之外的参数则抛出运行时异常。这与case
语句非常契合,因为我们在使用case
语句时常常是匹配一组具体的模式,最后用“_”来代表剩余的模式。如果一组case语句没有涵盖所有的情况,那么这组case
语句就可以被看做是一个偏函数。——译者注
除了这些基础知识,这里还有一些使用actor的最佳实践。编写代码应该尽量遵循下面几条规则:
把传入消息做成不可变的;
考虑把消息类型做成
case
类;不要在actor内部做阻塞操作,一个也别做。
不是每一个程序都需要遵守所有的最佳实践,但大多数应用程序应该都能从这些建议中受益。
对于更加复杂的actor,经常有必要控制它的启动和关闭。关闭actor通常都是用带有Boolean
条件判断的循环。如果你喜欢,也可以将actor写成函数式的风格,这样传入的消息就不会影响它的状态。
Scala对基于actor的并发编程提供的支持还有很多。我们在这里看到的只是皮毛。如果想全面了解,请参阅Nilanjan Raychaudhuri的大作Scala in Action(Manning, 2010)。