快学Scala第20章----Actor

本章详细介绍了Scala中的Actor系统,包括如何创建和启动Actor、发送与接收消息、Actor间的通信方式、消息通道、同步消息与Future、Actor的生命周期以及如何链接多个Actor。强调了Actor模型的异步特性、状态管理以及避免共享状态的重要性,提倡通过消息传递实现并发和解耦。
摘要由CSDN通过智能技术生成

本章要点

  • 每个actor都要扩展Actor类并提供act方法
  • 要往actor发送消息,可以用actor ! message .
  • 消息发送是异步的:“发完就忘”
  • 要接收消息,actor可以调用receive或react,通常是在循环中这样做。
  • receive/react的参数是由case语句组成的代码块(从技术上讲,这是一个偏函数)
  • 不同actor之间不应该共享状态。总是使用消息来发送状态
  • 不要直接调用actor的方法。通过消息进行通信。
  • 避免同步消息—也就是说将发送消息和等待响应分开
  • 不同actor可以通过react而不是receive来共享线程,前提是消息处理器的控制流转足够简单
  • 让actor挂掉是OK的,前提是你有其他actor监控着actor的生死。用链接来设置监控关系。

创建和启动Actor

actor是扩展自Actor特质的类。该特质带有一个抽象方法act。我们可以重写这个方法来指定该actor的行为。

import scala.actors.Actor

class HiActor extends Actor {
  def act() {
    while(true) {
      receive {
        case "Hi" => println("Hello")
      }
    }
  }
}

act方法和Java中Runnable接口中的run方法很相似,而且不同actor的act方法也是并行运行的。不同的是, actor针对响应消息做了优化,而线程可以开展任意的活动。
要启动一个actor,我们需要构造一个实例,并调用start方法:

val actor1 = new HiActor
actor1.start()

有时,我们需要临时创建actor而不是定义一个类。Actor伴生对象有一个actor方法来创建和启动actor:

import scala.actors.Actor._

val actor2 = actor {
  while(true) {
    receive {
      case "Hi" => println("Hello")
    }
  }
}

说明: 有时,一个匿名的actor需要向另一个actor发送指向自己的引用。这个引用可以通过self属性获取。


发送消息

actor是一个处理异步消息的对象。消息可以是任何对象。要发送消息,我们可以使用为actor定义的!操作符:

actor1 ! "Hi"

消息被送往该actor,当前的线程继续执行。我们也可以阻塞当前线程,直到等到回复(这并不常见)。
一个好的做法是使用样例类作为消息。这样一来,actor就可以使用模式匹配来处理消息。例如,我们有一个检查信用卡诈骗的actor。我们可能会想要向他发消息说某个记账动作正在执行。

case class Charge(creditCardNumber: Long, merchant: String, amount: Double)

// 样例类的一个对象发送给actor
fraudControl ! Charge(4111111111111111L, "Fred's Bait and Tackle", 19.95)

// 假定该actor的act方法包含一个如下语句
receive {
  case Charge(ccnum, merchant, amt) => ...
}

接收消息

发送到actor的消息被存放在一个“邮箱”中。receive方法从邮箱获取下一条消息并将它传递给它的参数,该参数是一个偏函数。例如:

receive {
  case Deposit(amount) => ...
  case Withdraw(amount) => ...
}

receive的代码块被转换成一个类型为PartialFunction[Any, T]的对象,其中T是case语句=>操作符右边的表达式的计算结果的类型。这是个“偏”函数,因为它只对那些能够匹配其中一个case语句的参数有定义。
说明: 消息投递的过程是异步的。所以你不能让你的程序依赖任何特定顺序的消息。
如果在receive方法被调用时并没有消息,则该调用会阻塞,直到有消息抵达。
如果邮箱中没有任何消息可以被偏函数处理,则对receive方法的调用也会阻塞,直到一个可以匹配的消息抵达。
注意: 邮箱可能被那些不与任何case语句匹配的消息占满。你可以添加一个 case _来处理任意的消息。
actor运行在单个线程中,在actor作用于内的资源是不会发生争用的。例如:

class AccountActor extends Actor {
  private var balance = 0.0

  def act() {
    while(true) {
      receive {
        case Deposit(amount) => balance += amount
        case Withdraw(amount) => balance -= amount
        ...
      }
    }
  }
}

**注意: **actor可以安全的修改它自己的数据。但是如果它修改了在不同actor之间共享的数据,那么争用状况就可能出现。
不要在不同的actor中使用共享的对象,这样为了同步会牺牲掉效率。除非你知道对这个对象的访问是线程安全的。


向其他Actor发送消息

当运算被拆分到不同actor来并行处理问题的各个部分时,这些处理结果最终要被收集到一起。actor可以将结果存入到一个线程安全的数据结构当中,比如一个并发的哈希映射。但是使用共享的数据结构会导致资源争用,为了同步,势必有的actor会被阻塞,这样计算效率就会下降。actor鼓励将计算结果通过消息的方式发送到另一个actor(这挺像在Qt中的使用信号和槽样)。
一个actor是如何知道应该往哪里发送计算结果的呢?这里有几个设计选择:
1. 可以有一些全局的actor。不过,当actor数量很多时,这个方案的伸缩性不好。
2. actor可以构造成带有指向一个或更多actor的引用。
3. actor可以接收带有指向另一个actor的引用的消息。在请求中提供一个actor引用是很常见的做法。例如:

actor ! Computer(data, continuation)  // continuation是另一个actor,当结果被计算出来的时候应该调用该actor
  1. actor可以返回消息给发送方。receive方法会把sender字段设为当前消息的发送方。

消息通道

除了在你的应用程序中,对actor共享引用的做法外,你还可以共享消息通道给它们。这样做的好处:
1. 消息通道是类型安全的—-你只能发送或接受某个特定类型的消息。
2. 你不会不小心通过消息通道调用到某个actor的方法。
消息通道可以是一个OutputChannel(带有!方法),也可以是一个InputChannel(带有receive或react方法)。Channel类同时扩展OutputChannel和InputChannel特质。
要构造一个消息通道,你可以提供一个actor:

val channel = new Channel[Int](someActor)

如果你不提供构造参数,那么消息通道就会绑定到当前执行的这个actor上

case class Computer(input: Seq[Int], result: OutputChannel[Int])
class Computer extends Actor {
  public void act() {
    while(true) {
      receive {
        case Computer(input, out) => { val answer = ...; out ! answer }
      }
    }
  }
}


actor {
  val channel = new Channel[Int]
  val computeActor: Computer = ...
  val input: Seq[Double] = ...
  computeActor ! Compute(input, channel)
  channel.receive {
    case x => ...
  }
}

同步消息和Future

actor 可以发送一个消息并等待回复,用!?操作符即可。例如:

val reply = account !? Deposit(1000)
reply match {
  case Balance(bal) => println("Current Balance: " + bal)
}

上面的代码发送消息后就会被阻塞,直到接收到回复。因此接收方必须返回一个消息给发送方:

receive {
  case Deposit(amount) => {
    balance += amount; sender ! Balance(balance)
    ...
  }
}

说明: 除了用sender ! Balance(balance),你也可以写reply(Balance(balance))。
注意: 同步消息容易引发死锁。通产而言,避免在actor的act方法里执行阻塞调用。
你也可以设置等待时间,超时后将会收到一个Actor.TIMEOUT对象。

actor {
  woker ! Task (data, self)
  receiveWithin(seconds * 1000) {
    case Result(data) => ...
    case TIMEOUT => log(...)
  }
}

除了等待对方返回结果之外,你也可以选择接收一个future—–这是一个将在结果可用时产出结果的对象。使用!!方法即可做到:

val replyFuture = account !! Deposit(1000)

isSet方法会检查结果是否已经可用。要接收该结果,使用函数调用的表达法:

val reply = replyFuture()

这个调用将会阻塞,直到回复被发送。


共享线程

如果程序中使用了大量的actor,而为每个actor创建一个单独的线程开销会很大。我们可以在同一个线程中运行多个actor,这样做的前提是每个消息处理函数只需要做比较小规模的工作,然后就继续等待下一条消息。
在Scala中,可以使用react方法接受一个偏函数,并将它添加到邮箱,然后退出。嘉定我们有两个嵌套的react语句:

react {  // 偏函数f1
  case Withdraw(amount) =>
  react {  // 偏函数f2
    case Confirm() =>
      println("Confirming " + amount)
  }
}

在这里,第一个react的调用将f1与actor的邮箱关联起来,然后退出。当Withdraw消息抵达时,f1被调用。偏函数f1也调用了react。这次调用把f2与actor的邮箱关联起来,然后退出。当Confirm消息到达时,f2被调用。
由于react会退出,因此你不能简单的将它放在while循环中。例如:

def act() {
  while(true) {
    react {
      case Withdraw(amount) => println("Withdrawing " + amount)
    }
  }
}

在这里,当act被调用时,对react的调用将f1与邮箱关联起来,然后退出。当f1被调用时,它将会处理这条消息。不过,f1没有任何办法返回到循环当中—–它只不过是一个小小的函数:

{ case Withdraw(amount) => println("Withdrawing " + amount) }

解决办法之一是在消息处理器中再次调用act方法:

def act() {
  react {
    case Withdraw(amount) => {
      println("Withdrawing " + amount)
      act()
    }
  }
}

这样做意味着用一个无穷递归替换掉无穷循环。这个递归不会占用很大的栈空间。每次对react的调用都会抛出异常,从而清栈。
有一些“控制流转组合子”可以自动产出这些循环。loop组合子可以制作一个无穷循环:

def act() {
  loop {
    react {
      case Withdraw(amount) => println("Withdrawing " + amount)
    }
  }
}

也可以使用循环条件:

loopWhile( count < max ) {
  react {
    ...
  }

}

Actor的生命周期

actor的act方法在actor的start方法被调用时开始执行。actor的如下情形之一会终止执行:
1. act 方法返回
2. act方法由于异常被终止
3. actor调用exit方法。
**说明: **exit方法是个受保护的方法,它只能被Actor的子类中调用。例如:

val actor1 = actor {
  while (true) {
    receive {
      case "Hi" => println("Hello")
      case "Bye" => exit()
    }
  }
}

其他方法不能调用exit来终止一个actor。
当actor因一个异常终止时,退出的原因就是UncaughtException样例类的一个实例。
- actor : 抛出异常的actor。
- message: Some(msg), 其中msg是该actor处理的最后一条消息;或者None,如果actor在没来得及处理任何消息之前就挂掉的话。
- sender: Some(channel), 其中channel是代表最后一条消息的发送方的输出消息通道;或者None,如果actor在没来得及处理任何消息之前就挂掉的话。
- thread: actor退出时所在的线程
- cause: 相应的异常。


将多个Actor链接在一起

如果你将两个actor链接在一起,则每以个都会在另一个终止执行的时候得到通知。要建立这个关系,只需要简单的以另一个actor的引用调用调用link方法:

def act() {
  link(master)
  ...
}

链接是双向的。如果监管actor在数个工作actor中分发工作任务,当某个工作actor挂掉的时候,监管actor应该知道,以便相应的工作可以被重新指派。反过来,如果监管actor挂掉了,工作actor也应该知道,以便可以停止工作(可以有多个actor实现HA,参考Zookeeper)。
注意: 尽管链接是双向的,但link方法并不对称。你不能将link(worker)替换成work.link(self)。该方法必须由请求链接的actor调用。
默认情况下,只要当前actor链接到的actor中有一个以非‘normal原因退出,当前actor就会终止。在这种情况下,退出原因和链接到的那个actor的退出的原因相同。
actor可以改变这个行为,做法是设置trapExit为true。这样修改后,actor会接收到一个类型为Exit的消息,该消息包含了那个正在终止的actor和退出的原因。

override def act() {
  trapExit = true
  link(worker)
  while(true) {
    receive {
      ...
      case Exit(linked, UncaughtException(_,_,_,_, cause)) => ...
      case Exit(linked, reason) => ...
    }
  }
}

在操作actor时,允许它们挂掉是很正常的。只需要把每个actor链接到一个监管actor,由它来处理失败的actor,例如,将它们的工作重新分派,或重新的启动它们。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值