Scala学习笔记--Actor和并发

时间:2021-03-20 08:15:08

感谢博主lyrebing  博文地址:http://blog.csdn.net/lyrebing/article/details/20446061

1.  Actor用法

1.1 Actor的基本使用

Scala会建立一个线程池共所有Actor来使用。

receive模型是Actor从池中取一个线程一直使用;

react模型是Actor从池中取一个线程用完给其他Actor用

例1-1 基本运行方式1

//actor是一个类似线程的实体,它有一个用来接收消息的信箱。
//实现actor的方法是继承Scala.actors.Actor并完成其act方法
//通过调用actor的start方法来启动它
class SillyActor extends Actor{
def act(){
for(i<- 1 to 5){
println("运行次数:"+i)
Thread.sleep(1000);
}
}
} object SimpleActor{
def main(args:Array[String]):Unit = { val sactor1 = new SillyActor();
sactor1.start(); //启动actor
}
}

运行结果:

运行次数: 1
运行次数: 2
运行次数: 3
运行次数: 4
运行次数: 5

例1-2: 基本运行方式2

object SimpleActor{
def main(args:Array[String]):Unit = {
//另一种方法:使用Actor中名为actor工具的方法来创建actor
//actor在定义后立即启动,无需在调用start()
val sactor2 = Actor.actor{
for(i<-1 to 5){
println("Another Actor"+i)
Thread.sleep(1000);
}
}
}
}

运行结果:

Another Actor1
Another Actor2
Another Actor3
Another Actor4
Another Actor5

1.2 发送消息

提示:

!

发送异步消息,没有返回值。

!?

发送同步消息,等待返回值。(会阻塞发送消息语句所在的线程)

!!

发送异步消息,返回值是 Future[Any]。

?

不带参数。查看 mailbox 中的下一条消息。

2.  接收消息

2.1  方式1:接受receive

特点:要反复处理消息,receive外层用while(..), 不用的话只处理一次。

例2-1: 使用receive接收、处理消息

//通过调用Actor.receive来接收消息
//actor发送消息时,它并不会阻塞,当actor接收消息时,它也不会被打断。
//发送的消息在接收actor的邮箱中等待处理,知道actor调用receive方法 val sactor3 = Actor.actor{
var work = true;
while(true){
Actor.receive{
case x :Int =>println("got an Int: "+x)
case _ =>println("not an Int");
}
}
} sactor3 ! 12
sactor3 ! 13
sactor3 ! 1.5

运行结果:

got an Int: 12
got an Int: 13
not an Int

2.2  方式2 接受react

特点:

(1) 从不返回;
(2) 要反复执行消息处理,react外层用loop,不能用while(..);
(3) 通过复用线程,比receive更高效,应尽可能使用react;

例2-2: 获得并显示IP网站的地址

ReactActor.java

object ReactActor{
def main(args:Array[String]):Unit={
NameResolver.start();
NameResolver ! ("www.baidu.com",Actor.self)
NameResolver ! "msg1";
NameResolver ! "EXIT"
NameResolver ! "msg2";//已经结束,不会显示
}
} object NameResolver extends Actor{
def act(){
loop{
react{
case (name:String, actor:Actor)=>
println(getIp(name))
case "EXIT" =>
println("Name resorver exit!");
exit; //跳出loop
case msg =>
println("Unhandled message" + msg);
}
}
}
/* //不使用loop的方法
def act(){
react{
case (name:String, actor:Actor)=>
println(getIp(name))
//actor ! getIp(name);
act(); //再次调用act函数
case "EXIT" =>
println("Name resorver exit!");
case msg =>
println("Unhandled message" + msg);
act();
}
}
*/ //获取IP地址
def getIp(name:String):Option[InetAddress]={
try{
Some(InetAddress.getByName(name))
}catch{
case _:UnknownHostException =>None
}
}
}

运行结果:

Some(www.baidu.com/119.75.217.109)
Unhandled messagemsg1
Name resorver exit!

3. 良好的Actor风格

3.1 Actor不应阻塞

编写良好的actor在处理消息时不应阻塞。阻塞的问题是,在actor阻塞时,另一个actor可能会对其发起另一个它能够处理的请求。如果actor在首个请求时阻塞了,那么它将不会留意到第二个请求。最坏的情形是可能带来死锁,多个actor都在等待另一个阻塞的actor的响应。

actor{
Thread.sleep(time)
mainActor ! "WAKEUP"
}

这个助手actor的确阻塞了,但由于它永远不会受到消息,因此在这种情况下是可以的。主actor可以继续响应新的请求,下面程序emoteLater方法展示了这种处理方式的用法。它创建一个新的actor来执行sleep以便主actor不阻塞,以确保它向正确的actor发送"Emote"消息,我们必须小心地在主actor中对self求值而不是在助手actor中。

object SimpleActor2Copy{

  def emoteLater(){ //辅助actor,用于休眠
val mainActor = Actor.self;
Actor.actor{
Thread.sleep(1000);
mainActor ! "Emote"
}
} def main(args:Array[String]):Unit={
val sActor2 = Actor.actor{
var emoted = 0;
var con = true;
emoteLater(); //启动一次辅助actor
Actor.loopWhile(con){//Actor.loop用来重复执行一个代码块 ,loopWhile可以加上判断条件
Actor.react{
case "Emote" =>
println("I am acting "+emoted)
emoted+=1;
if(emoted<5){
emoteLater();
}else{
con = false
} case msg =>
println("received" + msg)
}
}
}
}
}

运行结果:

I am acting 0
I am acting 1
I am acting 2
I am acting 3
I am acting 4

由于这个actor并不在sleep方法中阻塞--它的助手actor会阻塞--他可以在等待下次表演之前继续做其他事。

3.2 actor之间只通过消息进行通信。

actor模型让我们写多线程程序时只用关注各个独立的单线程程序(actor),他们之间通过消息来通讯。例如,如果BadActor中有一个GoodActor的引用:

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

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

3.3 采用不可变消息

由于Scala的actor模型提供了在每个actor的act 方法中的单线程环境,不必担心在这个方法的实现中使用的对象是否是线程安全的。
每个act方法实际上被局限在一个线程中,在act方法中你可以尽情使用非同步、可变对象,actor模型被称作share-nothing的模型,因为数据局限于一个线程中,而不是被多个线程共享。
有一个例外:用于在actor间发送消息的对象中的数据由多个actor“共享”。这时要关注消息对象是否安全。

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

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

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

3.4 让消息自说明

对每一种消息创建一个对应的case class,而不是使用上面的tuple数据结构。虽然这种包装在很多情况下并非必须,但该做法能使actor程序易于理解,例如:

// 不易理解,因为传递的是个一般的字符串,很难指出那个actor来响应这个消息
lookerUpper ! ("www.scala-lang.org", self)
// 改为如下,则指出只有react能处理LoopupIP的actor来处理:
case class LookupIP(hostname: String, requester: Actor)
lookerUpper ! LookupIP("www.scala-lang.org", self)

4. 不同JVM间的消息访问

服务器端:

object MyServer{
def main(args:Array[String]):Unit={ Actor.actor { // 创建并启动一个 actor
// 当前 actor 监听的端口: 3000
RemoteActor.alive(3000)
// 在 3000 端口注册本 actor,取名为 server1。
// 第一个参数为 actor 的标识,它以单引号开头,是 Scala 中的 Symbol 量,
// Symbol 量和字符串相似,但 Symbol 相等是基于字符串比较的。
// self 指代当前 actor (注意此处不能用 this)
RemoteActor.register('server1, Actor.self); // 收到消息后的响应
Actor.loop {
Actor.react {
case msg =>
println("server1 get: " + msg)
}
}
} }
}

客户端:

import scala.actors.Actor
import scala.actors.remote.RemoteActor
import scala.actors.remote.Node object SimpleActor2Copy{
def main(args:Array[String]):Unit={
Actor.actor {
// 取得一个节点(ip:port 唯一标识一个节点)
// Node 是个 case class,所以不需要 new
val node = Node("127.0.0.1", 3000) // 取得节点对应的 actor 代理对象
val remoteActor = RemoteActor.select(node, 'server1) // 现在 remoteActor 就和普通的 actor 一样,可以向它发送消息了!
println("-- begin to send message")
remoteActor ! "ActorClient的消息"
println("-- end") }
}
}

运行结果:

控制台输出客户端发送的消息  “server1 get: ActorClient的消息”