30.5        好的actor编程风格(P616

前面已经展示了actor的完整用法,本节展示actor一些好的用法和编程风格,按照这种风格写的并发程序可以更易纠错,更少出现死锁和竞用冲突。

l  actor不应阻塞

写得好的actor处理消息时不阻塞,因为一旦actor阻塞了,其他actor给该actor发的消息可能就无法处理,如果该actor在处理第一条消息时就阻塞,它就处理不了后续消息了。如果多个actor都因等待其他actor的响应而阻塞了,就可能发生更糟糕的情况——死锁。

actor不应阻塞,而应该能处理多个消息,这一般需要其他actor的协助。例如,原来要直接调用Thread.sleep(此处用来模拟费时操作)来阻塞当前actor的地方,可以创建一个helperactor,让helper actorsleep一段时间后发消息回来,对比如下程序(1)、(2):

  import actors._, actors.Actor._

  val time = 1000

 

  // 1原来阻塞的程序

  val mainActor1 = actor {

    loop { react {

        case n: Int => Thread.sleep(time)

                         println(n)

        case s => println(s) } }

  }

  1 to 5 foreach { mainActor1 ! _ } // 5秒钟后打印完数字

 

  // 2改写由helper actor去阻塞的程序

  val mainActor2: Actor = actor {

    loop { react {

        case n: Int => actor { Thread.sleep(time); mainActor2 ! "wakeup" }

                        println(n)

        case s => println(s) } }

  }

  1 to 5 foreach { mainActor2 ! _ } // 马上打印数字; 1秒钟后打印5wakeup

 

程序(2)中的helper actor虽然是阻塞的,但他不接受任何消息,所以这么用也没问题。有了helper actor的帮助,mainActor2就能及时处理到达的消息,而不会象mainActor1那样阻塞等待。

注释:react的工作原理

返回类型为Nothing就表明一个函数不会正常返回,而是以exception异常来中断,react就是这么干的。react的实际实现比较复杂,但可以从概念上认为它是这么工作的:

当你调用actorstart方法时,有线程会调用actoract方法,如果act方法中有reactreact就会从actor的信箱中查找能分配到某个case分支去处理的消息(receive也如此处理)。如果发现能分配处理的消息,react就让该case分支处理消息并抛一个异常;如果没有发现能分配处理的消息,react就把actor“冷藏”起来直至信箱中有新的消息到来,并抛一个异常。就是说无论有没有消息处理,react都会最后抛个异常,其外层act会接到这个异常,调用此act的线程catch这个异常,但不做任何处理。

这就是为何react要想处理一条以上的消息,就必须在case分支中再次调用上层act,或者其他方式(loop)以达到再次调用react的目的。

 

l  actor之间用且仅用消息来通讯

actor模型的关键就是用安全可靠的act方法来解决“共享数据-锁”模型的困难,也就是说,actor让我们写多线程程序时只用关注各个独立的单线程程序,他们之间通过消息来通讯,这种对问题的简化是基于我们在actor之间仅用消息来通讯。

例如,如果BadActor中有一个GoodActor的引用:

class BadActor(a:GoodActor) extends Actor {...}

那在BadActor中即可以通过该引用来直接调用GoodActor的方法,也可以通过“!”来传递消息。如果通过引用,BadActor可以读取GoodActor实例的私有数据,而这些数据可能正被其他线程改写值,结果就是:你还是避免不了“共享数据-锁”模型中的麻烦事,即必须保证BadActor线程读取GoodActor的私有数据时,GoodActor线程在这块成为“共享数据”的操作上加锁。GoodActor只要有了共享数据,就必须来加锁防范竞用冲突和死锁,你又得从actor模型退回到“共享数据-锁”模型(注:actor对消息是顺序处理的,本来不用考虑共享数据)。

另一方面,这也并不意味着你永远只用消息传递,尽管把“共享数据-锁”弄正确很难但也不是不可能。ScalaErlang在对待actor上的差异,就在于使用Scala,同一个程序中你可以选择组合使用actor和“共享数据-锁”。

例如,想要多个actor共享一个公共的可变map,一种方案即纯粹的actor方案是创建一个actor,管理这个可变map,并定义一组让其他actor存取该可变map的消息:往该共享map中放入key-value对的消息,从在共享map中由key获取value的消息,等等,还可以定义发送异步响应给之前发来请求的actor的消息。另一种方案,可以使用java.util.concurrt包中已经定义好的ConcurrentHashMap,让所有actor直接使用该共享map

尽管用actor实现共享map比自己从头实现ConcurrentHashMap要简单安全得多,但实际上ConcurrentHashMap已经存在了,而且库代码质量也不错,很容易判断直接使用ConcurrentHashMap更容易、风险更小。当然,ConcurrentHashMap实现的是同步的共享map,你可以用actor实现异步的共享mapScalaactor库给你这种选择权。

注释:如果考虑使用“共享数据-锁”

当考虑是否组合使用actor和“共享数据-锁”模型时,下面的电影(1971年的警匪片“肮脏的哈里”)台词获取能提供点帮助:(乱译的,没看懂)

我知道你在想:“他到底会开6枪还是5枪?”老实告诉你,我兴奋得有点失控了。但这是只世界上最牛的×××,能打爆你的脑袋,你应该问自己:“我是否够幸运?”来吧,小子。