原创

3、Akka传递消息

1 消息传递

Akka 有 4 种核心的 Actor 消息模式: tell 、ask 、forward 和 pipe。

  • Ask:向 Actor 发送一条消息,返回一个 Future。当 Actor 返回响应时,会返回 Future。不会向消息发送者的邮箱返回任何消息。
  • Tell:向 Actor 发送一条消息。所有发送至 sender()的响应都会返回给发送消息的 Actor。
  • Forward:将接收到的消息再发送给另一个 Actor。所有发送至 sender() 的响应都会返回给原始消息的发送者。
  • Pipe:用于将 Future 的结果返回给 sender() 或 另外一个 Actor。如果正在使用 Ask 或是处理一个 Future,那么使用 Pipe 可以正确地返回 Future 的结果。

2 消息不可变

可变消息可能偶尔会在某个时刻给程序造成一些无法描述的坏情况。因此要保证消息最好是不可变的,不仅要保证引用不可变,也要保证类型不可变。使用 final 关键字可以保证引用不可变,使用 String 替代 StringBuffer 可以保证类型不可变。因为 String 是不可变的,但是 StringBuffer 的内容是追加的。
在这里插入图片描述
  无论什么时候,只要需要在线程之间共享数据,就应该首先考虑将消息定义成不可变的。

Ask 消息模式

Ask 模式会生成一个 Future,表示 Actor 返回的响应。Actor 系统外部的普通对象与 Actor 通信时经常会使用这种方式。在调用 Ask 向 Actor 发起请求时,Akka 实际上会在发起方的 Actor 系统中创建一个临时的 Actor。接收请求的 Actor 在返回响应时使用的 sender()引用就是这个临时的 Actor。当一个 Actor 接收到 ask 请求发来的消息并返回响应时,这个临时 Actor 会使用返回的响应来完成 Future。
在这里插入图片描述
  因为 sender()引用就指向临时 Actor 的路径,所以 Akka 知道用哪个消息来完成 Future。 Ask 模式要求定义一个超时参数,如果对面没有在超时参数限定的时间内返回这个响应,那么 Future 就会返回失败。
在这里插入图片描述
Ask 模式看上去很简单, 不过它是有隐藏的额外性能开销的。 首先, ask 会导致 Akka 在/temp 路径下新建一个临时 Actor。 这个临时 Actor 会等待从接收 ask 消息的 Actor 返回的响应。 其次, Future 也有额外的性能开销。 Ask 会创建 Future, 由临时 Actor 负责完成。
这个开销并不大,但是如果需要非常高频地执行 ask 操作,那么还是要将这一开销考虑在内的。Ask 很简单,不过考虑到性能,使用 tell 是更高效的解决方案。

Tell 消息模式

Tell 是最简单的一种消息模式。Tell 通常被看做是一种 fire-and-forget 消息传递机制,无需指定发送者;不过通过一些巧妙的方法,也可以使用 tell 来完成request/reply风格的消息传递
在这里插入图片描述
  Tell 是 ActorRef/ActorSelection 类的一个方法。它可以接收一个响应地址作为参数,接收消息的 Actor 中的 sender()就是这个响应地址。在 Scala 中,默认情况下,sender 会被隐式定义为发送消息的 Actor。如果没有 sender(如在 Actor 系统外部发送请求),那么响应地址不会默认设置为任何邮箱(叫做 DeadLetters)。
  在 Java 中并没有隐式定义或是默认参数,所以必须提供 sender。如果不想指定任何特定的 sender 作为响应地址,那么应该遵循如下写法:

  • 从一个 Actor 内部发送消息,使用 self()。 如:actor.tell(message,self())
  • 从 Actor 系统外部发起消息,使用 noSender()。如:actor.tell(message,ActorRef.noSender())

相较于ASK,使用 Tell 处理响应

我们可以在 Actor 中将一些上下文信息存储在一个 map 中,将 map 的 key 放在消息中一起发送。然后,当有着相同 key 的消息返回时, 就可以恢复上下文, 完成消息的处理了。 这就允许我们能够使用类似 ask 模式的语义,又可以避免 ask 造成的额外性能开销。
在这里插入图片描述
乍一看这种做法的性能开销似乎比使用 ask 时还要大,但是如果要把许多个 Actor 组合起来,这种做法就省去了使用 ask 时的超时以及创建额外 Actor 的问题。这样我们就能够控制超时发生的位置。
由于 tell 不需要提供超时参数,所以我们通常都会想要在某些时候及时生成超时信息。

Forward 消息模式

Tell 在语义上是用于将一条消息发送到另一个 Actor ,并将响应地址设置为当前 Actor。而Forward不是,初始发送者保持不变,只不过新增了一个收件人。使用 Forward 传递消息时,响应地址就是原始消息的发送者。
在这里插入图片描述

Pipe

有时候需要将 Actor 中的某个 Future 返回给请求发送者。sender()是一个方法,所以要在 Future 的回调函数中访问 sender(),我们必须存储一个指向 sender() 的引用:

 final ActorRef senderRef = sender();
 future.map(x -> {
            senderRef.tell(x,ActorRef.noSender())
   });

Future 的回调函数(比如上面的 map)会在另一个线程中执行,所以未必能够通过 sender()访问到正确的值。这就是需要将 sender()存储起来的原因。但是这个原因并不清晰。比如我最开始疑惑为什么不直接将 sender()写到 map 函数中,而使用上面存储起来的引用。
而通过使用 Pipe 模式,就可以避免上面的疑惑。Pipe 会得到一个 Future 结果,然后将 Future 的结果返回给 sender()。

pipe(future,system.dispatcher()).to(sender());

pipe 接收 Future 的结果作为参数,然后将其传递给所提供的 Actor 引用。因为这里的 sender() 执行在当前线程上,所以可以直接调用 sender()。不用像之前那样存储 sender()的引用。

参考文献

《Akka入门与实践》 – 第三章 传递消息

关注公众号:Data Porter 回复Akka领取《Akka入门与实践》书籍

正文到此结束
本文目录