Actor 是"私有状态 + 消息收件箱"的并发单元:不共享内存、不加锁,所有交互都是异步消息。Scala 生态的实现从 Akka 演化到开源继承者 Pekko,本节讲模型本身与它的适用边界。
多线程共享一个计数器,经典做法是加锁。锁的问题清单很长:忘锁、锁粒度、死锁、优先级反转,而且每个 bug 都在并发压力下才现身。Actor 的思路是釜底抽薪——让每个状态只被一个执行体触碰:
// 概念示意(Pekko 风格 API) class Counter extends Actor: private var count = 0 // 私有,外界摸不到 def receive: Receive = case "inc" => count += 1 case "get" => sender() ! count // 回消息
外界只能发消息(counter ! "inc"),消息在收件箱排队、逐条处理。count 永远只被 Actor 自己的线程读写,锁从设计里消失了——不是被更聪明地使用,而是被取消。
限价订单簿是典型的高状态密度场景:买卖单频繁插删、成交配对必须原子。用共享 Map 加锁写,配对逻辑与锁边界纠缠;用 Actor 写,订单簿的整个状态就是 Actor 的私有字段,"撮合一批订单"天然原子:
case class Limit(side: Side, price: BigDecimal, qty: Int) case class Match(side: Side, price: BigDecimal, qty: Int) class OrderBook extends Actor: private var bids = SortedMap.empty[BigDecimal, Int] private var asks = SortedMap.empty[BigDecimal, Int] def receive = case l @ Limit(Bid, p, q) => bids += p -> (bids.getOrElse(p, 0) + q); tryMatch() case l @ Limit(Ask, p, q) => asks += p -> (asks.getOrElse(p, 0) + q); tryMatch() private def tryMatch(): Unit = ??? // 读自己的 bids asks,无锁撮合
模式匹配(3.3)在这里正好做消息分派,样例类(2.1)正好做消息载体——前几章的装备全在肩上。
| 优点 | 代价 |
|---|---|
| 无锁、状态天然原子 | 单 Actor 吞吐有上限(消息排队) |
| 天然分布式叙事(消息可跨节点) | 调试栈被消息切断,需要因果追踪 |
| 背压、监督、容错有成熟模式 | 学习与运维成本高于 Future 管线 |
选型经验:无状态并行用 Future(6.1);状态密集、需横向扩展时才上 Actor。大多数业务系统里 Actor 的合理位置是少数几个"热点状态点"(撮合、会话、连接管理),而不是把每个 service 都包成 Actor——后者只是把函数调用换成排队,徒增延迟。
监督策略是 Actor 体系的另一价值:子 Actor 崩溃时由父级决定重启或上报,故障处理本身成为架构的一部分而非散落的 try-catch。
背景:下游接口每秒只吃 10 个请求,直接并发打过去会被熔断。用单个 Actor 做令牌闸门,所有请求先向它领票:
import akka.actor.typed.*, ActorRef enum Gate: case TryPass(replyTo: ActorRef[Boolean]) case Return class GateActor(n: Int) extends Actor[Gate]: private var tokens = n def receive(msg: Gate): Unit = msg match case Gate.TryPass(reply) => if tokens > 0 then { tokens -= 1; reply ! true } else reply ! false case Gate.Return => tokens += 1 // 使用方:请求前问一次,失败走延迟重试;请求完成后归还一张票
操作:请求方发送 TryPass 并等待回执,拿到 true 才放行,处理完发 Return 归还。结果:无论多少并发线程,令牌数只被这一个 Actor 串行修改,不存在检查与扣减之间的竞态窗口。解读:Actor 把"锁保护状态"改写成"状态只归一个线程"——不是消灭竞争,而是让竞争在邮箱里排队。变式:Akka 的 ask 模式、Pekko(Akka 的开源继任者)API 几乎相同,迁移成本低;纯标准库场景可用 本章前面学过的原子引用替代,但状态一多 Actor 仍是最清晰的建模。
Actor 默认邮箱无上限,下游处理慢时消息无限堆积最终 OOM。处置清单:配置 bounded mailbox 让上游收到背压;监控 dead letters 日志——发给已停止 Actor 的消息会进死信队列,批量死信几乎总意味着生命周期管理有 bug;ask 超时要设置,否则一个卡死的下游会拖住整条调用链。这些都是上线后才补课的贵知识,营地阶段先记清单。