本章内容
- 定义一个 Actor 及其行为
- 实例化 Actor 并向其发送消息
- 用变量在 Actor 内保存状态
- 用行为在 Actor 内保存状态
- 按计划(定时)发送消息
本章将把上一章的 Akka 基础落到实处:创建 Actor、向 Actor 发送消息、设置 Actor 的行为;还会讲如何处理状态,以及如何按计划延迟发送消息。
我们从一个最小的 Akka 应用开始:一个可以往里"存钱"的钱包(wallet)。它可以作为更大"体育赛事投注"应用的钱包子系统。你会在本书后面学习如何实现这个投注应用,但首先要把基础打牢。
注意 要运行本书示例,请按附录 A 的说明安装所有工具。安装完成后再回到这里开始。
本章会实现几种"钱包"变体,借此串讲一些 Akka 原语:
- 最开始,钱包仅把存入的钱打印到控制台,不维护总额。
- 随后,你将学习如何把金额存进变量。
- 最后,你会实现钱包的"停用/启用"。
注意 本章源代码见 www.manning.com/books/akka-... 或
github.com/franciscolo...。任意代码片段/清单都能在同名.scala
文件里找到,例如 object Wallet
的片段在 Wallet.scala
中。
2.1 打印金额
本节你将创建一个应用和一个 Actor,并向该 Actor 发送两条消息,随后它会把消息输出到控制台。示例如下:Wallet$
是 Actor 的类名,后面是 Actor 的日志------每发送一条消息就一行(回忆:Actor 的行为像一个消息队列):
scss
[Wallet$] - received '1' dollar(s)
[Wallet$] - received '10' dollar(s)
接下来直接看组成该应用的 Actor 与主类。
2.2 开始编码
创建 Actor 前,必须先声明它能接收的消息类型。不能"来者不拒",你需要定义 Actor 的协议(protocol)。
2.2.1 Actor 的协议
Akka API 通过 Behavior[Int]
指定 Wallet Actor 的行为(只能接收 Int
型消息)。这就是它的协议。本例中,Wallet Actor 的协议类型为 Int
,定义如下:
arduino
import akka.actor.typed.Behavior
object Wallet {
def apply(): Behavior[Int] = ...
}
此时 Actor 的其余代码并不重要。实现可以很多样,但接下来开发的关键在于:明确该 Actor 能接收哪些消息。
2.2.2 创建应用并实例化 Actor
协议确定后,就可以通过上一章讲到的 ActorSystem
创建应用并添加 Actor。应用就是一个可执行程序。你可以定义一个继承自 scala.App
的对象来创建它:
scala
object WalletApp extends App
这是一种创建可运行类的简单方式。最后会讲如何运行。
接下来关注 ActorSystem
构造时的签名,尤其是第一个参数:
scala
object WalletApp extends App {
val guardian: ActorSystem[Int] = ActorSystem(Wallet(), "wallet") ❶
}
❶ 实例化一个 Akka 应用
在 Scala 中,泛型(等同于 Java 的泛型)用方括号标注。下面是 ActorSystem
的 apply
(构造)方法,其泛型参数用 [T]
表示:
less
def apply[T](guardianBehavior: Behavior[T], name: String): ActorSystem[T]
注意 在 Scala 中,像 ActorSystem
这样的对象通过 apply
创建,也可以仅写对象名调用。比如 ActorSystem(myParameters)
实际调用的是 ActorSystem.apply(myParameters)
。同理,Wallet()
等价于 Wallet.apply()
。
所以,ActorSystem(Wallet(), "wallet")
的第一个参数就是 Wallet
的构造(apply
)。
有了 Actor,下面开始发消息。
2.2.3 发送消息
这里有两点要注意。其一,Wallet Actor 被实例化成 ActorSystem
,且类型为 [Int]
,表明它只能 接收 Int
消息。其二,上一章提到过,ActorSystem
是一个指向 Actor 的引用,即 ActorRef
------你可以用它给 Actor 发消息。发送使用 tell
方法,通常用"感叹号"操作符 !
表示。比如:
myActorReference ! 1
把这些拼在一起,下面的代码定义并实例化了 Actor,并发送两条消息:
scala
import akka.actor.typed.ActorSystem
object WalletApp extends App {
val guardian: ActorSystem[Int] = ActorSystem(Wallet(), "wallet") ❶
guardian ! 1 ❷
guardian ! 10 ❷
}
❶ 创建 Actor
❷ 向 Actor 发送消息
首先发送整数 1
,然后发送整数 10
。Actor 的类型是 Int
(ActorSystem[Int]
中已显式声明),从代码能成功编译也可以验证------如果你给不接收 Int
的 Actor 发送 Int
,编译就会报错,应用根本运行不起来。这个类型检查(尤其是在重构时改变了 Actor 协议,而旧协议仍在某处使用时由编译器提示)是 Akka Typed 相比旧版本的巨大提升。接下来看看 Actor 如何接收消息。
2.2.4 实现 Actor:接收消息
如前所述,Actor 的完整类型是 Behavior[Int]
。实现行为要用行为工厂(behavior factories)。Behaviors.receive
是一个工厂函数,输入是 context
和 message
(分别是 ActorContext
与收到的 Int
),输出是一个 Behavior
。其签名如下:
csharp
object Wallet {
def apply(): Behavior[Int] =
Behaviors.receive { (context, message) => ❶
...
}
}
❶ Behaviors.receive
的签名
在工厂中可以使用 message
和 context
,例如用 context
的日志功能打日志:
csharp
object Wallet {
def apply(): Behavior[Int] =
Behaviors.receive { (context, message) =>
context.log.info(s"received '$message' dollar(s)") ❶
...
}
}
❶ 记录每条收到的消息
最后,需要为下一条 消息指定行为。这里可以继续使用同一个行为,对应工厂是 Behaviors.same
。合在一起,Actor 的最终实现如下:
csharp
object Wallet {
def apply(): Behavior[Int] =
Behaviors.receive { (context, message) =>
context.log.info(s"received '$message' dollar(s)")
Behaviors.same ❶
}
}
❶ 下一条消息仍用相同的行为处理
注意这些 Behaviors
像"套娃"一样嵌套:外层 def apply(): Behavior[Int]
包含一个 Behaviors.receive
,其内部又返回 Behaviors.same
。你会反复看到这种模式。
2.2.5 终止系统
应当优雅地 终止 ActorSystem
,以便系统能关闭持有的模块资源(比如 HTTP、数据库连接等)。做法是对 guardian(ActorSystem
)调用 .terminate()
。
因为示例在 sbt
shell 中运行,你可以加个钩子来触发终止,比如监听一次回车:使用 scala.io.StdIn.readLine()
,按下 Enter 即触发:
scala
object WalletApp extends App {
...
println("Press ENTER to terminate")
scala.io.StdIn.readLine()
guardian.terminate()
}
程序会在 .readLine()
处阻塞等待。你按回车后,继续执行终止系统。
注意 sbt
shell 是用来编译/运行 sbt
项目的交互式 shell。安装 sbt
后,在命令行输入 sbt
即可进入。在其中可以编译、构建、测试并运行 Scala/Java/C/C++ 程序。
2.2.6 应用程序
结合前面的内容,完整代码如下。
代码清单 2.1 钱包完整代码
scala
object WalletApp extends App {
val guardian: ActorSystem[Int] = ActorSystem(Wallet(), "wallet") ❶
guardian ! 1 ❷
guardian ! 10 ❷
println("Press ENTER to terminate")
scala.io.StdIn.readLine()
guardian.terminate()
}
object Wallet {
def apply(): Behavior.Receive[Int] = ❸
Behaviors.receive { (context, message) =>
context.log.info(s"received '$message' dollar(s)")
Behaviors.same
}
}
❶ 实例化一个 Actor
❷ 向 Actor 发送 Int
类型消息
❸ 定义 Wallet Actor 如何响应每条消息的行为
2.2.7 Git 中的解决方案
该钱包实现在 akka-topics
仓库中。克隆项目后,在根目录可以看到 build.sbt
,sbt
用它来编译、运行和测试。项目包含多个子项目(每章一个),便于你边读边跑、边测、边改。每个子项目包含:
- 名称 ------用一个
val
定义 - 文件位置 ------相对于
build.sbt
的子项目文件夹名 - Scala 版本 ------设置后由
sbt
自动下载 - 库依赖 ------子项目用到的库,同样由
sbt
自动下载
本章所有示例实现于 chapter02
子项目,其在 build.sbt
中的定义如下:
less
val chapter02 = project
.in(file("chapter02")) ❶
.settings(
scalaVersion := "2.13.1", ❷
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.20", ❸
"ch.qos.logback" % "logback-classic" % "1.2.3"
)
)
...
❶ 子项目所在的文件夹
❷ Scala 版本
❸ Akka Typed 模块
本书围绕 Akka Typed 展开,这里引用的就是其核心模块。
2.2.8 运行应用
要运行钱包应用,打开命令行进入 akka-topics
根目录,启动 sbt
shell:
ruby
$ sbt
你应看到类似清单 2.2 的输出。大多数内容可以忽略,最重要的是第一行(sbt
与 Java 版本)和最后一行(sbt server started
,说明进入了 sbt
shell)。
清单 2.2 启动 sbt shell
css
[info] welcome to sbt 1.5.0 (AdoptOpenJDK Java 11.0.9) ❶
[info] loading settings for project global-plugins from build.sbt ...
[info] loading global plugins from /Users/francisco/.sbt/1.0/plugins
[info] loading settings for project code-build from plugins.sbt ...
[info] loading project definition from /Users/francisco/AkkaBook/code/project
[info] loading settings for project code from build.sbt ...
[info] resolving key references (24537 settings) ...
[info] set current project to code (in build file:/Users/francisco/AkkaBook/code/)
[info] sbt server started at local:///Users/francisco/.sbt/1.0/server/347d55a887437ea58247/sock
[info] started sbt server ❷
sbt:code>
❶ sbt 与 Java 版本
❷ 表示 server 已启动
build.sbt
中已经定义了 chapter02
子项目(GitHub: github.com/franciscolo... )。因此在 sbt
shell 中可以选中该子项目并运行:
markdown
> project chapter02
> run
你的输出应类似如下,会列出当前子项目(chapter02
)中可运行的程序,让你选择其一:
清单 2.3 子项目 chapter02 中可运行的应用
less
[info] compiling 13 Scala sources to
➥/Users/francisco/AkkaBook/code/chapter02/target/scala-2.13/classes ...
Multiple main classes detected. Select one to run:
[1] com.manning.WalletApp
[2] com.manning.WalletOnOffApp
[3] com.manning.WalletStateApp
[4] com.manning.WalletTimerApp
Enter number:
输入 1
选择 WalletApp
,即可看到运行时的输出。
2.3 用变量保存状态
接下来你将学习如何给钱包增加状态 ,以便记录钱包中的总金额。状态管理是 Akka 最重要的能力之一。在分布式并发环境里,管理状态是个硬问题;而在 Akka 中更容易一些,因为你是在 Actor 内按顺序、逐条消息地管理状态。我们来看看怎么做。
状态可以由**行为(behavior)**来实现------也就是函数。在这个示例里,钱包把数值存到 total
,该值可以在一定范围内增减。为简化起见,最大值设为 2,最小值设为 0(零)。当超过最大/最小值时,钱包就不再增/减,并且 Actor 停止运行,以表示钱包进入了不可接受的状态。
你已经知道什么是**协议(protocol)**了。此前我们用"整数"作为协议类型只是为了演示;真实场景的协议通常由若干类型组成,它们同属一个层级,并以一个 sealed trait 作为顶层。
定义 :trait
类似于 Java 的接口。关于 trait 参见 docs.scala-lang.org/tour/traits...。sealed trait 允许编译器检查你的模式匹配是否穷尽 ;不穷尽会有编译期告警。可参见 "Sealed types" 一节:mng.bz/Pz8v。
因此,这个钱包不用 Behavior[Int]
,而是 Behavior[Command]
。它的协议包含两类消息:Increase(amount: Int)
与 Decrease(amount: Int)
,二者都继承自 Command
:
scala
object WalletState {
sealed trait Command
final case class Increase(amount: Int) extends Command
final case class Decrease(amount: Int) extends Command
...
}
你可能会问,为什么把协议类型命名为 Command
?这是一种常见命名,便于一眼看出哪些消息是请求 Actor 执行动作的。并非所有消息都是命令,你会在下一章看到例子。
注意 你可能注意到:给 Increase
传一个负数也能"减少"总额。要避免这种情况,可以在读取时统一取绝对值,或在数值非法时直接报错。
钱包的初始值是 0,最大值是 2。这就是为什么清单 2.4 里用工厂方法 WalletState(0, 2)
。该 Actor 在创建时接收入参(创建方式见清单之后的说明)。创建好钱包后,就可以按其协议发送消息。为简洁,这里只展示递增。
代码清单 2.4 创建钱包的应用
scala
object WalletStateApp extends App {
val guardian: ActorSystem[WalletState.Command] =
ActorSystem(WalletState(0, 2), "wallet-state") ❶
guardian ! WalletState.Increase ❷
guardian ! WalletState.Increase ❷
guardian ! WalletState.Increase ❷
}
❶ 初始总额为 0,最大值为 2
❷ 发送消息
像之前一样,在命令行中运行:
markdown
> project chapter02
> run
选择 3,WalletStateApp。你会看到如下输出。钱包会把总额增加到上限,然后"过载":
ruby
INFO WalletState$ - increasing to 1
INFO WalletState$ - increasing to 2
INFO WalletState$ - I'm overloaded. Counting '3' while max is '2'. Stopping.
要把"总额"和"最大值"存进 Actor,你需要学点新东西:如何在创建 Actor 时传入初始值。在前一个示例里,我们创建 Actor 时没有传参。
首先,把两个"变量"(初始总额与最大值)加到 Actor 的构造器里------也就是 Wallet
的 def apply
方法。严格说它们不是"变量",在 Scala 里叫值(不可变变量)。添加后的签名如下:
less
def apply(total: Int, max: Int): Behavior[Command]
其次,必须让这两个入参的值能从一次消息处理 保留到下一次 (即从一次调用保留到下一次调用),从而在第一次递增之后,在下一条递增消息到来时还能拿到更新后的 total
与 max
。要做到这一点,就不能再用 Behaviors.same
返回同一个行为,而是要直接再次调用 apply(current, max)
并传入更新后的值(钱包还包含更多逻辑,这里只展示维持 total
与 max
这两个状态所需的关键部分):
arduino
object WalletState {
def apply(total: Int, max: Int): Behavior[Command] =
Behavior.receive { (context, message) => ❶
...
val current = total + 1 ❷
...
apply(current, max) ❸
}
}
❶ 输入函数
❷ 递增总额
❸ 指定下一步的行为,同时带上新的 total
与 max
因为行为就是函数 ,你可以用它们的输入与输出把状态从一个函数(一次行为)"传递"到下一个。这里,在 def apply(total: Int, max: Int)
的末尾返回 apply(current, max)
,意味着下一条消息仍由 apply
来处理,但带着更新过的入参。第一次时,total
与 max
是通过 .apply(0, 2)
(即 WalletState(0, 2)
,感谢 Scala 的语法糖)设置的;而处理了一条 Increase
后,下一次的行为就是 .apply(1, 2)
。通过这种方式,行为获得了新的入参,状态也就从一条消息传递到下一条消息。
最后,当达到 max
时,Actor 通过把下一步行为设置为 Behaviors.stopped
来停止。
注意 一旦 Actor 停止,它将无法再处理任何消息。其邮箱中剩余或之后发送到其地址的消息,系统会转发给名为 deadLetters
的 Actor(第 3 章详述)。另外要注意:apply()
不是"递归函数",而是一个工厂 :它接收一个函数作为输入,输出一个 Behavior
(一个函数)。定义 apply
的工厂 Behaviors.receive
接收一个 (context, message) => Behavior
类型的函数,并返回一个 Behavior
。这是一种函数式风格,不涉及递归。
来看钱包在收到 Increase
时的日志与状态管理。
代码清单 2.5 带状态的钱包 Actor
scala
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
object WalletState {
sealed trait Command
final case class Increase(amount: Int) extends Command
final case class Decrease(amount: Int) extends Command
def apply(total: Int, max: Int): Behavior[Command] = ❶
Behaviors.receive { (context, message) =>
message match {
case Increase(amount) =>
val current = total + amount
if (current <= max) {
context.log.info(s"increasing to $current")
apply(current, max) ❷
} else {
context.log.info(
s"I'm overloaded. Counting '$current' while
➥max is '$max'. Stopping")
Behaviors.stopped ❸
}
case Decrease(amount) => ❹
...
}
}
}
❶ 输入参数 total
与 max
共同构成状态
❷ 返回"下一步行为",而不是调用某个方法
❸ 停止 Actor。邮箱里剩余的消息将不会到达该 Actor。
❹ 与 Increase
类似,只是逻辑相反
回忆一下:当钱包递增且当前值未超过最大值时,通过把下一步行为设置为 WalletState.apply(current, max)
来继续增加。从第一条到第二条消息,行为从 .apply(0, 2)
变为 .apply(0 + 1, 2)
。
反过来,Decrease
在钱包值变成负数时也会停止。代码几乎一样,这里不再重复,感兴趣可在仓库中查看。
注意协议是写在 Actor 内部的。协议消息总是特定于某个 Actor,从这个意义上讲,它们并不打算被其它 Actor 复用。它们就是该 Actor 的 API,理应属于它。
本节你学会了如何把状态作为值 在 Actor 中管理:把状态加到构造器里,并把它传递给"下一步行为"。不过,还有另一种在 Actor 中保存状态的方式:用行为本身来存储状态。
2.4 用行为(behaviors)保存状态
在 Actor 中用不同行为 来保存状态,与用变量 保存状态的目的不同。使用行为是为了表达:Actor 在不同状态下对同一类消息会有不同反应。如果你只是想在 Actor 内部改动变量,不要用这种方式;请使用上一节介绍的方法。
根据 Actor 所处的状态,同一条消息可能产生完全不同的效果。比如,一个充当代理(proxy)的 Actor 可以有两个状态:开启(open)与关闭(closed) 。开启时,它会把消息转发到另一处;关闭时,它不会转发,而是通知第三方。
我们再实现一个钱包。与前一个变体类似,为简单起见,这里只支持增加 金额,不支持减少。新变化在于:这个钱包有两个状态,对应两个行为:activated(已启用)与deactivated(已停用) 。当钱包处于启用状态,收到 Increase
时与前例一样会增加总额;当钱包处于停用状态,总额不可更改,收到 Increase
会丢弃并报告未变更。
注意 这种特性让该 Actor 成为一个有限状态机(FSM) ,因为它对同样的输入会有不同反应。简单说,FSM 具有有限个状态;根据所处状态(也称行为),相同输入可能产生不同结果。Actor 以行为来定义,天然非常适合实现这种模型。
为简化,钱包不再有最大值,也不支持减少;初始总额默认为 0。其协议如下:
scala
object WalletOnOff {
sealed trait Command
final case object Increase(amount: Int) extends Command
final case object Activate extends Command
final case object Deactivate extends Command
...
}
实例化该钱包无需入参,因为没有最大值且初始金额默认为 0。你可以在停用 与启用 两种状态下都给它发 Increase
。注意默认状态为启用。下面的代码展示了先增加、再停用、再启用、再增加:
scala
object WalletOnOffApp extends App {
val guardian: ActorSystem[WalletResume.Command] =
ActorSystem(WalletOnOff(), "wallet-on-off")
guardian ! WalletResume.Increase(1) ❶
guardian ! WalletResume.Deactivate ❷
guardian ! WalletResume.Increase(1) ❸
guardian ! WalletResume.Activate ❹
guardian ! WalletResume.Increase(1) ❺
}
❶ 增加钱包金额
❷ 停用钱包
❸ 钱包处于停用,金额不会增加
❹ 启用钱包
❺ 增加钱包金额
在相同子项目下,于 sbt shell 选择选项 2(WalletOnOffApp
)运行该程序,输出如下:
css
INFO [WalletOnOff$] - increasing to 1
INFO [WalletOnOff$] - wallet is deactivated. Can't increase
INFO [WalletOnOff$] - activating
INFO [WalletOnOff$] - increasing to 2
这些功能都可以用你在本章已见过的方式实现。通过多写几个类似 .apply
的方法,你就能拥有多个行为,并在它们之间切换。
要实现"默认总额为 0,默认状态为启用",可以这样写:
css
object WalletOnOff {
...
def apply(): Behavior[Command] = activated(0)
activated(0)
...
}
**启用(activated)行为在收到 Increment
时增加总额,收到 Deactivate
时切换到 停用(deactivated)**行为。钱包定义了 activated
与 deactivated
两个行为。先看启用态。
清单 2.6 WalletOnOff
中的 activated
行为
scss
object WalletOnOff {
...
def activated(total: Int): Behavior[Command] =
Behaviors.receive { (context, message) =>
message match {
case Increase(amount) => ❶
val current = total + amount
context.log.info(s"increasing to $current")
activated(current)
case Deactivate => ❷
deactivated(total)
case Activate => ❸
Behaviors.same ❸
}
}
}
❶ 增加总额并记录日志
❷ 切换到停用行为
❸ 保持相同行为与相同总额
你也许会问:在收到 Activate
时,为何返回 Behavior.same
而不是 activated(total)
?因为这里没有发生增加操作,行为完全不变,用 Behavior.same
更直观(当然也可以传回同一个 total
,但 Behavior.same
更易读)。
**停用(deactivated)**行为如下:收到 Increase
时只记录"无法增加",收到 Activate
时回到启用态。停用时总额不变。
清单 2.7 deactivated
行为
typescript
object WalletOnOff {
...
def deactivated(total: Int): Behavior[Command] = {
Behaviors.receive { (context, message) =>
message match {
case Increase => ❶
context.log.info(s"wallet is deactivated. Can't increase")
Behaviors.same
case Deactivate => ❷
Behaviors.same
case Activate => ❸
context.log.info(s"activating")
activated(total)
}
}
}
}
❶ 记录无法增加
❷ 保持相同行为与相同总额
❸ 切回启用行为(总额不变)
整个 Actor 代码如下。
清单 2.8 WalletOnOff
Actor
scala
object WalletOnOff {
sealed trait Command
final case class Increase(amount: Int) extends Command
final case object Deactivate extends Command
final case object Activate extends Command
def apply(): Behavior[Command] = activated(0) ❶
def activated(total: Int): Behavior[Command] = ❷
Behaviors.receive { (context, message) =>
message match {
case Increase(amount) =>
val current = total + amount
context.log.info(s"increasing to $current")
resume(current)
case Deactivate =>
deactivated(total)
case Activate =>
Behaviors.same
}
}
def deactivated(total: Int): Behavior[Command] = { ❸
Behaviors.receive { (context, message) =>
message match {
case Increase =>
context.log.info(s"wallet is deactivated. Can't increase")
Behaviors.same
case Deactivate =>
Behaviors.same
case Activate =>
context.log.info(s"activating")
activated(total)
}
}
}
❶ 默认行为
❷ 启用态行为
❸ 停用态行为
本节展示了如何在 Akka Typed 中创建一个 FSM。Akka 的原语本身就具备 FSM 语义------你可以说,Actor 模型的公理 就包含了这些语义。你在第 1 章见过这条公理:"Actor 必须指定用于处理下一条消息的行为。"Akka 将这条公理直接落地,因此你只用最基本的构件------行为------就能构建 FSM。
在并发环境里,以"等待时间"的思路来编程极易出错。Akka 简化了这件事:与其让 Actor 主动等待(像线程那样),不如给它排程发送消息。
2.5 调度一条消息(Scheduling a message)
我们再实现一个与前一款非常相似的钱包。除了一个差异外,其他一切相同:这个钱包可以接收"停用若干秒"的消息,从而在一段时间内被停用。
有了这种停用方式,钱包的使用者无需记得再发"启用"消息来恢复;钱包在被停用后,会在数秒后给自己发送一条"启用"消息 。具体等待多久由之前收到的停用消息中的持续时间来决定。请注意,这里没有任何阻塞------Actor 端不会"等待",而是像往常一样继续处理消息;调度只是在指定时间点把消息发出。
协议如下所示:
scala
object WalletTimer {
sealed trait Command
final case class Increase(amount: Int) extends Command
final case class Deactivate(seconds: Int) extends Command
private final case object Activate extends Command
...
}
与之前不同的是,Deactivate
现在带有持续时间参数;而 Activate
被标记为 private
,因此无法从 Actor 外部激活钱包 ,只能由钱包自己激活。该变体命名为 WalletTimer。
注意 在 Akka 中,将协议区分为公共 与私有 是常见模式。这样你既可以提供给其他 Actor 使用的公共 API (一组消息),也能保留只处理 Actor 内部机能的私有 API。
使用这个新钱包,可以这样创建应用:
scala
object WalletTimerApp extends App {
val guardian: ActorSystem[WalletTimer.Command] =
ActorSystem(WalletTimer(), "wallet-timer")
guardian ! WalletTimer.Increase(1)
guardian ! WalletTimer.Deactivate(3)
}
在相同子项目的 sbt shell 中选择选项 4(WalletTimerApp
)运行,输出如下:
ini
10:10:49,896 INFO [WalletTimer$] - increasing to 1
10:10:52,914 INFO [WalletTimer$] - activating ❶
❶ 3 秒后恢复
要实现对 Activate
的定时发送 ,需要用到工厂 Behaviors.withTimers
创建定时器构建器 ,再用其 .startSingleTimer
启动一次性 定时器。这部分本身就是一个 Behavior
,可以像其他行为(activated
、deactivated
)一样组合使用。
启动一次性定时器的方法签名如下,其中 T
是 Actor 协议类型,FiniteDuration
是延迟时长的类型:
less
def startSingleTimer(msg: T, delay: FiniteDuration): Unit
通过 import scala.concurrent.duration._
,整数可自动转为 FiniteDuration
(例如秒),写法如 .second
。
清单 2.9 WalletTimer 的 activated
行为
scss
import scala.concurrent.duration._
object WalletTimer {
...
def activated(total: Int): Behavior[Command] =
Behaviors.receive { (context, message) =>
Behaviors.withTimers { timers => ❶
message match {
case Increase(amount) =>
val current = total + amount
context.log.info(s"increasing to $current")
resume(current)
case Deactivate(t) =>
timers.startSingleTimer(Activate, t.second) ❷
deactivated(total)
case Activate =>
Behaviors.same
}
}
}
}
❶ 用工厂创建定时器
❷ 调度在 t
秒后发送 Activate
。将 t: Int
转为以秒计的 FiniteDuration
钱包的其余部分可以保持不变;加入消息调度后,就具备了新功能。很简单,对吧?
小结(Summary)
-
ActorSystem 是第一个 Actor,同时代表整个系统。
-
你应当优雅终止
ActorSystem
,以便系统能关闭所有持有资源的模块(如 HTTP 或数据库连接)。 -
tell
操作符(其简写为bang :!
)用于向 Actor 发送消息。 -
对于 Actor 处理的每一条消息 ,都需要指定下一条消息使用的行为。由于这个抽象,你只需关心一次处理一条消息,从而消除了并发问题的一个维度。
-
Behaviors 是嵌套的:就像函数链式调用一样,一个行为的输出成为下一个行为的输入,共同构成 Actor 的完整行为。
-
多条消息通常组成 Actor 的协议(protocol) 。这是 Actor 的 API,也是其他 Actor 与之通信的唯一方式。
-
Actor 有两种方式保存状态:
- 作为值保存在构造参数中,并在收到消息时更新;
- 作为行为 保存在多个行为之间(对同类消息作出不同响应),也就是有限状态机(FSM) 。
-
Behaviors.withTimers
允许你调度消息 ,即指定消息何时 、以何频率发送。