我的演示应用很简单。这是演员:
class CounterActor extends Actor {
@volatile private[this] var counter = 0
def receive: PartialFunction[Any, Unit] = {
case Count(id) ⇒ sender ! self ? Increment(id)
case Increment(id) ⇒ sender ! {
counter += 1
println(s"req_id=$id, counter=$counter")
counter
}
}
}
主要应用:
sealed trait ActorMessage
case class Count(id: Int = 0) extends ActorMessage
case class Increment(id: Int) extends ActorMessage
object CountingApp extends App {
// Get incremented counter
val future0 = counter ? Count(1)
val future1 = counter ? Count(2)
val future2 = counter ? Count(3)
val future3 = counter ? Count(4)
val future4 = counter ? Count(5)
// Handle response
handleResponse(future0)
handleResponse(future1)
handleResponse(future2)
handleResponse(future3)
handleResponse(future4)
// Bye!
exit()
}
我的经纪人:
def handleResponse(future: Future[Any]): Unit = {
future.onComplete {
case Success(f) => f.asInstanceOf[Future[Any]].onComplete {
case x => x match {
case Success(n) => println(s" -> $n")
case Failure(t) => println(s" -> ${t.getMessage}")
}
}
case Failure(t) => println(t.getMessage)
}
}
如果我运行应用程序,我会看到下一个输出:
req_id=1, counter=1
req_id=2, counter=2
req_id=3, counter=3
req_id=4, counter=4
req_id=5, counter=5
-> 4
-> 1
-> 5
-> 3
-> 2
处理回复的顺序是随机的。这是正常的行为吗?如果不是,我该如何订购?
PS 的
我是否需要演员中的volatile var?
PS2
另外,我正在为handleResponse
寻找一些更方便的逻辑,因为这里的匹配非常含糊......
答案 0 :(得分:3)
正常行为?
是的,这绝对是正常行为。
您的演员按照您发送的顺序收到计数增量,但期货正在通过提交到底层线程池完成。这是Future-thread绑定的不确定排序,导致println执行失序。
如何订购?
如果你想要有序执行Futures,那么它就是同步编程的同义词,即根本没有并发性。
我需要挥发性吗?
Actor的状态只能在Actor本身中访问。这就是为什么Actor的用户永远不会得到一个真正的Actor对象,他们只得到一个ActorRef,例如val actorRef = actorSystem actorOf Props[Actor]
。这部分是为了确保Actors的用户永远不能通过消息传递更改Actor的状态。来自docs:
好消息是,Akka演员在概念上各有自己的 轻质螺纹,完全与其余部分隔离 系统。这意味着不必使用同步访问 锁定你可以编写你的演员代码而不用担心 并发性。
因此,您不需要挥发性。
更方便的逻辑
为了更方便的逻辑,我建议使用Agents
,它是一种带有更简单消息框架的类型化Actor。来自docs:
import scala.concurrent.ExecutionContext.Implicits.global
import akka.agent.Agent
val agent = Agent(5)
val result = agent()
val result = agent.get
agent send 7
agent send (_ + 1)
读取是同步但即时的。写作是异步的。这意味着无论何时进行读取,您都不必担心Futures,因为内部值立即返回。但是一定要阅读文档,因为排队逻辑可以使用更复杂的技巧。
答案 1 :(得分:2)
您的方法中的实际问题不是异步性质,而是过于复杂的逻辑。
尽管拉蒙的答案非常明确,但是我确定有一些方法可以确保akka某些部分的秩序。我们可以从the doc读取,每个发件人 - 收件人对 保证 消息排序。
这意味着,对于两个演员的每个单向频道都有保证,消息将按照发送的顺序发送。
但是,对于您用来处理答案的Future
任务完成顺序,没有这样的保证。将Future
从ask
作为消息发送给原始发件人是很奇怪的。
你可以这样做:
将您的Increment
重新定义为
case class Increment(id: Int, requester: ActorRef) extends ActorMessage
所以处理程序可以知道原始请求者
修改CounterActor
的接收为
def receive: Receive = {
case Count(id) ⇒ self ! Increment(id, sender)
case Increment(id, snd) ⇒ snd ! {
counter += 1
println(s"req_id=$id, counter=$counter")
counter
}
}
将handleResponse
简化为
def handleResponse(future: Future[Any]): Unit = {
future.onComplete {
case Success(n: Int) => println(s" -> $n")
case Failure(t) => println(t.getMessage)
}
}
现在您可能会看到以相同的顺序收到消息。
我说可能因为处理仍然在Future.onComplete
中发生,所以我们需要另一个演员来确保订单。
让我们定义额外的消息
case object StartCounting
演员本身:
class SenderActor extends Actor {
val counter = system.actorOf(Props[CounterActor])
def receive: Actor.Receive = {
case n: Int => println(s" -> $n")
case StartCounting =>
counter ! Count(1)
counter ! Count(2)
counter ! Count(3)
counter ! Count(4)
counter ! Count(5)
}
}
在您的main
中,您现在可以写
val sender = system.actorOf(Props[SenderActor])
sender ! StartCounting
扔掉handleResponse
方法。
现在你肯定应该以正确的顺序看到你的消息处理。
我们实现了整个逻辑,没有单ask
和that's good。
如此神奇的规则是:将处理回应留给演员,只通过ask
获得最终结果。
注意还有forward
method,但这会创建代理actor,因此消息排序将再次被破坏。