l  采用不可变消息

Scalaactor模型让每个actoract方法内部接近于单线程环境,你不用当心act方法里面的操作是否线程安全。在act方法中你可以尽情使用非同步、可变对象,因为每个act方法被有效限制在单个线程中,这也是actor模型被称为“share-nothing 模型(零共享模型)的原因,其数据的作用范围被限制在单个线程中。

但“share-nothing”规格有个例外情况:对象内的数据被用于多个actor之间进行消息传递。这时你就必须考虑消息对象是否线程安全。

保证消息对象线程安全的最好方法就是保证只使用不可变对象作为消息对象。消息类中只定义val字段,且只能指向不可变对象。定义这种不可变消息类的简单方法就是使用case class 并保证其所有的val字段都是不可变的。Scala API中提供了很多不可变对象可用,例如基本类型、StringTupleList,不可变Set、不可变Map等。

当然,如果一个actor发送可变的、没做同步的对象作为消息,但从不读写该对象也不会出问题,但这纯属自找麻烦。程序的后续维护者可能不会意识到该对象是共享对象,就有可能去写该对象,这会产生能以查找的并发Bug

总之,最好保证程序数据中那些没做同步的可变对象只由单个actor来存取。当然,如果愿意,可以在不同actor之间传递对象,但要确保在特定时间清楚地知道哪个actor拥有该对象并能存取它。换句话说,当你设计基于actor的系统时,必须决定哪块可变内存(数据结构)是指派给哪个actor的,所有其他actor要存取这块可变内存都只能发送消息给该可变内存的唯一主人,并等待消息回应。

如果你发现确实需要把一个可变对象obj1发送给其他actor,也因该是发送一份拷贝对象obj1.clone过去,而不是把obj1直接发过去。例如,数据对象Array是可变且未做同步的,所以Array只应该由一个actor同时存取,如果需要发送数组arr,就发送arr.clonearr中的元素也应该是不可变对象),或者直接发送一个不可变对象arr.toList更好。

大部分时候使用不可变对象很方便,不可变对象是并行系统的曙光,它们是易使用、低风险的线程安全对象。当你将来要设计一个和并行相关的程序时,无论是否使用actor,你都应该尽量使用不可变的数据结构。

 

l  让消息自说明

当从方法返回一个值时,调用者可以根据返回值接着调用之前的状况继续后续处理。

但使用actor时就不是这么回事了,发出请求消息的actor不应该阻塞,应该边接着干其他事边等待回应消息,当回应消息到达时如何中断来处理就是个难题了,它能够记得发出请求消息之前在干嘛呢?

简化actor程序逻辑的一种途径是在消息中包含冗余信息。如果请求消息是一个不可变对象,可以在回应消息中包含一个请求消息的引用!例如:获取域名IPactor,返回消息中不光包含IP,也包含请求消息中对应的域名,这样做只增加了一小点代码量,但简化了actor的处理逻辑:

  def act() {

    loop {

      react {

        case (name: String, actor: Actor) =>

          actor ! (name, getIp(name))

      }

    }

  }

 

另一种在消息中增加冗余信息的途径,是对每一种消息创建一个对应的case class,而不是使用上面的tuple数据结构。当然这种包装在很多情况下并非必须,但这能使actor程序易于理解,例如:

// 不易理解,因为传递的是个一般的字符串,很难指出那个actor来响应这个消息

lookerUpper ! ("www.scala-lang.org", self)

// 改为如下,则指出只有react能处理LoopupIPactor来处理:

case class LookupIP(hostname: String, requester: Actor)

lookerUpper ! LookupIP("www.scala-lang.org", self)

程序员只要在源码中全文搜索“LookupIP”,就能找到如下文件中NameResolver2是处理该消息的(而处理String类型的actor可能很多):

// 完整程序,把请求消息和回应消息进行包装

  import actors.Actor._, actors._, java.net._

 

  case class LookupIP(name: String, respondTo: Actor)

  case class LookupResult(name: String, address: Option[InetAddress])

 

  def getIp(name: String): Option[InetAddress] = {

    try { Some(InetAddress.getByName(name))

    } catch { case _: UnknownHostException => None }

  }

 

  object NameResolver2 extends Actor {

    def act() {

      loop {

        react {

          case LookupIP(name, actor) =>

            actor ! LookupResult(name, getIp(name))

        }

      }

    }

  }

 

// 使用

scala>NameResolver2 ! LookupIP("g.cn", self)                                               

scala>self.receiveWithin(0){case LookupResult(n,a)=>println(n,a.get)}

(g.cn,g.cn/203.208.39.104)

 

30.1        更长的例子:并发的离散事件模拟器(P623

18章有个离散事件模拟器例子,本节把它改写为并发模拟。每个模拟参与者运行自己的actor,这样可以利用多核来提高模拟器速度。本章以Philipp Haller开发的并发电路模拟器代码为基础,完成该改造。

(太繁,略)

30.2        结论(P640

并发编程给你巨大的力量,不仅能简化代码(?)还能利用多核的好处,但很不幸,广泛使用的并发原子、线程、锁、监测器也是死锁和资源竞用冲突的雷区。

actor风格另辟蹊径避开此雷区,让你在写并发程序的时候免于死锁和竞用冲突的巨大风险。本章介绍了Scalaactor的基本用法:如何创建actor,如何发送和接收消息,如何用react来保护线程等等。