在本文中,我们将对actor的创建进行介绍,并主要讲解一下Ask模式。虽然在Actor中,更受欢迎的是Tell模式,但是我们还是有必要了解一下稍微复杂一点的Ask模式。
一、Actor的创建
一个actor对象是由actorOf()方法创建的,需要一个Props参数和一个名字(字符串)。
object DemoActor {
/**
* Create Props for an actor of this type.
*
* @param magicNumber The magic number to be passed to this actor’s constructor.
* @return a Props for creating this actor, which can then be further configured
* (e.g. calling `.withDispatcher()` on it)
*/
def props(magicNumber: Int): Props = Props(new DemoActor(magicNumber))
}
class DemoActor(magicNumber: Int) extends Actor {
def receive = {
case x: Int => sender() ! (x + magicNumber)
}
}
class SomeOtherActor extends Actor {
// Props(new DemoActor(42)) would not be safe
context.actorOf(DemoActor.props(42), "demo")
// ...
}
上面是Akka官方推荐的构建方式,一个class用来定义Actor,一个object用来提供props方法。Props的内容会在之后的学习中,在进行详细的研究。
二、Ask模式
我们在上面的代码中,可以看到如下的消息传递方式:
sender() ! (x + magicNumber)
在这里,一个actor在receive函数中,向消息的源actor发送消息,使用tell(符号为:!)。这就是Tell模式,这个模式理解起来很简单,就是给别的actor发送一个消息,之后不阻塞,不记忆(事情没有发生过的样子)。没有等待消息回应的机制,接收到消息的actor往往会使用sender()方法,向消息的传递者回应消息。
而我们要讲的Ask模式,需要等待一个消息回应。而在等待回应的时候,我们可以定义一个等待的时间。我们看一下ask的函数签名:
def ask(message: Any)(implicit timeout: Timeout, sender: ActorRef = Actor.noSender): Future[Any]
我们可以使用一个implicit变量来自动填充timeout域。package actors_1
import akka.actor.{Actor, ActorSystem, Props}
import scala.concurrent.duration._
import akka.pattern.ask
import akka.util.Timeout
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
class NoRespond extends Actor{
override def receive: Receive = Actor.emptyBehavior
}
object AskNoRespond extends App{
val system = ActorSystem("AskNoRespond")
val actor = system.actorOf(Props[NoRespond], "norespond")
implicit val timeout = Timeout(2 second)
·
(actor ? "Hello") andThen {
case Success(_) => println("success")
case Failure(err) => println((s"error: $err"))
}
}
在代码中,我们设置timeout为2000ms,令actor对所有消息都不处理。我们可以得到如下的错误:
error: akka.pattern.AskTimeoutException: Ask timed out on [Actor[akka://AskNoRespond/user/norespond#-1340240777]] after [2000 ms]. Sender[null] sent message of type "java.lang.String".
由于在2000ms内,actor没有回复,所以Ask模式返回的Future不再阻塞,从而得到一个Failure(),注意这个Failure在scala.util包下。如果成功的话,大家可以自行对NoRespond类的receive函数进行修改。
现在我们介绍一下Ask模式中,经常使用的pipeTo方法。
import akka.pattern.{ ask, pipe }
import system.dispatcher // The ExecutionContext that will be used
final case class Result(x: Int, s: String, d: Double)
case object Request
implicit val timeout = Timeout(5 seconds) // needed for `?` below
val f: Future[Result] =
for {
x <- ask(actorA, Request).mapTo[Int] // call pattern directly
s <- (actorB ask Request).mapTo[String] // call by implicit conversion
d <- (actorC ? Request).mapTo[Double] // call by symbolic name
} yield Result(x, s, d)
f pipeTo actorD // .. or ..
pipe(f) to actorD
上面的代码是Akka官方给出的一个实例,我们就用这个进行讲解。使用for来构造Result对象。在for的初始化域当中,分别使用3种不同的方式调用ask()方法。对得到的Future对象使用mapTo转换成对应的类型。最终使用x,y,d构造Result。
上述的全部过程都是非阻塞的,所以在Result还没有生成的时候,Result对象f由pipeTo函数传递给另一个actor(actorD)。当Result组装完成(出现超时),onComplete handler将其通过消息传递给actorD。
以下的代码是稍微修改上面的代码,代码能够执行:
package actors_1
import akka.actor.Status.Failure
import akka.actor.{Actor, ActorIdentity, ActorLogging, ActorSystem, Identify, Props}
import akka.pattern.{ask, pipe}
import akka.util.Timeout
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
final case class Result(x: Int, s: String, d: Double)
case object Request
class Mediator extends Actor with ActorLogging {
implicit val timeout = Timeout(5 seconds)
val actorA = context.actorOf(Props[A1], "actorA")
val actorB = context.actorOf(Props[A2], "actorB")
val actorC = context.actorOf(Props[A3], "actorC")
override def receive: Receive = {
case Request =>
(for {
x <- (actorA ? Request).mapTo[Int]
y <- (actorB ? Request).mapTo[String]
d <- (actorC ? Request).mapTo[Double]
} yield Result(x, y, d)).pipeTo(sender())
log.info(s"Answer an request from $sender")
}
}
class A1 extends Actor with ActorLogging {
var value = 10
override def receive: Receive = {
case Request =>
sender() ! value
value += 1
}
}
class A2 extends Actor {
var value = "Hello"
override def receive: Receive = {
case Request =>
sender() ! value
value += "i"
}
}
class A3 extends Actor {
var value = 10.23
override def receive: Receive = {
case Request =>
sender() ! value
value += 1
}
// override def receive: Receive = Actor.emptyBehavior
}
class Origin extends Actor with ActorLogging {
override def receive: Receive = {
case Request =>
context.actorSelection("/user/med") ! Identify(1)
case ActorIdentity(1, Some(ref)) =>
ref ! Request
case r@Result(_, _, _) => log.info(s"$r")
case Failure(err) => log.error(s"$err")
case _ => log.error("Unexpected Massage")
}
}
object AskDemo extends App {
val system = ActorSystem("AskDemo")
val origin1 = system.actorOf(Props[Origin], "origin1")
val origin2 = system.actorOf(Props[Origin], "origin2")
val mediator = system.actorOf(Props[Mediator], "med")
origin1 ! Request
origin2 ! Request
}
在上面的代码中,我们创建了两个Origin来发起Request。它们分别向同一个Mediator发送Request消息。(这里使用了actorSelection用来进行actor的查找)Mediator在初始化的时候创建了3个子actor用来计算(A1,A2,A3)。他们分别计算结果,并通过pipeTo传递给对应的origin。[INFO] [07/28/2017 23:19:29.347] [AskDemo-akka.actor.default-dispatcher-4] [akka://AskDemo/user/med] Answer an request from Actor[akka://AskDemo/user/origin2#-1912347482]
[INFO] [07/28/2017 23:19:29.348] [AskDemo-akka.actor.default-dispatcher-4] [akka://AskDemo/user/med] Answer an request from Actor[akka://AskDemo/user/origin1#577293972]
[INFO] [07/28/2017 23:19:29.355] [AskDemo-akka.actor.default-dispatcher-4] [akka://AskDemo/user/origin1] Result(11,Helloi,11.23)
[INFO] [07/28/2017 23:19:29.355] [AskDemo-akka.actor.default-dispatcher-2] [akka://AskDemo/user/origin2] Result(10,Hello,10.23)
我们可以从输出得到如下结论:
1. 前两个Info当中,ask、mapTo、pipeTo操作全部是非阻塞的。
2. 子actor(A1,A2,A3)返回的结果都直接被传递到对应的origin当中,并且由于消息的局部顺序性,所以orgin1和origin2得到的结果是一致的。
三、总结
在本文中,我们对Actor中的Ask模式进行了介绍。由于其中使用了一些Future方面的知识,所以需要读者自行补充一下。(主要指导andThen和mapTo方法的用法)。其中Ask模式和pipeTo经常一起使用,能够直接将Future放到另一个actor中进行等待(Futre的阻塞)。而pipeTo的实现还需要进一步的学习和研究。
Feel free to point out my mistakes.