40、深入理解 Scala 中的 Akka 演员模型

深入理解Scala中Akka演员模型

深入理解 Scala 中的 Akka 演员模型

1. 演员模型基础

演员(Actor)在处理消息时可能会创建新的演员,并且可以改变其处理消息的方式,这实际上实现了状态机中的状态转换。与使用方法调用的传统对象系统不同,演员消息发送通常是异步的,因此操作的全局顺序是不确定的。演员可以控制一些状态,并根据消息对其进行演化。设计良好的演员系统会防止其他代码直接访问和修改这些状态,或者至少会强烈阻止这种做法。

这些特性使得演员可以并行运行,甚至可以跨集群运行。它们提供了一种管理全局状态的原则性方法,在很大程度上(但不是完全)避免了传统多线程并发的问题。

2. Akka:Scala 的演员库

在早期,Scala 自带了一个演员库,但现在这个库已被弃用,Akka 成为了 Scala 中基于演员的并发的官方库。Akka 由 Typesafe 开发和支持,并且还提供了全面的 Java API。

2.1 演员模型的重要实现

演员模型有两个重要的生产就绪实现:Erlang 实现和 Akka。Akka 的灵感来自于 Erlang 的实现,它们都实现了一个重要的创新,即强大的错误处理和恢复模型。

2.2 监督策略

在 Akka 中,不仅会创建演员来执行系统的常规工作,还会创建监督者(Supervisor)来监视一个或多个演员的生命周期。如果一个演员失败(例如抛出异常),监督者会遵循恢复策略,包括重启、关闭、忽略错误或委托给上级处理。

重启策略有两种:
- 全为一策略(All - for - one) :当失败的演员与其他演员紧密协作,且都在同一个监督者之下时,最好重启所有演员。
- 一对一策略(One - for - one) :当被管理的演员是独立的工作者,一个演员的失败不会影响其他演员时,只需要重启失败的演员。

这种架构将错误处理逻辑与正常处理逻辑清晰地分离,实现了全架构的错误处理策略,并且提倡“让它崩溃”的原则。

3. 示例介绍

我们将使用一个模拟客户端接口调用服务的示例,该服务将工作委托给工作者。客户端接口(包含 main 方法)名为 AkkaClient,它将用户命令传递给单个 ServerActor,ServerActor 再将工作委托给多个 WorkerActors,以确保不会阻塞。每个工作者模拟一个分片数据存储,维护一个键(Long 类型)和值(String 类型)的映射,并支持 CRUD(创建、读取、更新和删除)语义。

3.1 消息定义

首先,我们来看定义演员之间交换的所有消息的 Messages 对象:

// src/main/scala/progscala2/concurrency/akka/Messages.scala
package progscala2.concurrency.akka
import scala.util.Try

object Messages {
  sealed trait Request {
    val key: Long
  }
  case class  Create(key: Long, value: String) extends Request
  case class  Read(key: Long) extends Request
  case class  Update(key: Long, value: String) extends Request
  case class  Delete(key: Long) extends Request
  case class  Response(result: Try[String])
  case class  Start(numberOfWorkers: Int = 1)
  case class  Crash(whichOne: Int)
  case class  Dump(whichOne: Int)
  case object DumpAll
}

上述代码中消息的含义如下:
| 消息类型 | 含义 |
| ---- | ---- |
| Request | 所有 CRUD 请求的父特征,都使用 Long 类型的键 |
| Create | 使用指定的键和值创建一个新的“记录” |
| Read | 读取给定键的记录 |
| Update | 使用给定键的新值更新记录(如果不存在则创建) |
| Delete | 删除给定键的记录(如果不存在则不做任何操作) |
| Response | 封装响应,使用 scala.util.Try 表示成功或失败 |
| Start | 开始处理,发送给 ServerActor 并告知要创建的工作者数量 |
| Crash | 发送消息模拟一个工作者“崩溃” |
| Dump | 发送消息“转储”单个工作者或所有工作者的状态 |
| DumpAll | 转储所有工作者的状态 |

3.2 AkkaClient 实现

// src/main/scala/progscala2/concurrency/akka/AkkaClient.scala
package progscala2.concurrency.akka
import akka.actor.{ActorRef, ActorSystem, Props}
import java.lang.{NumberFormatException => NFE}

object AkkaClient {
  import Messages._
  private var system: Option[ActorSystem] = None

  def main(args: Array[String]) = {
    processArgs(args)
    val sys = ActorSystem("AkkaClient")
    system = Some(sys)
    val server = ServerActor.make(sys)
    val numberOfWorkers = sys.settings.config.getInt("server.number-workers")
    server ! Start(numberOfWorkers)
    processInput(server)
  }

  private def processArgs(args: Seq[String]): Unit = args match {
    case Nil =>
    case ("-h" | "--help") +: tail => exit(help, 0)
    case head +: tail => exit(s"Unknown input $head!\n"+help, 1)
  }

  private def processInput(server: ActorRef): Unit = {
    val blankRE = """^\s*#?\s*$""".r
    val badCrashRE = """^\s*[Cc][Rr][Aa][Ss][Hh]\s*$""".r
    val crashRE = """^\s*[Cc][Rr][Aa][Ss][Hh]\s+(\d+)\s*$""".r
    val dumpRE = """^\s*[Dd][Uu][Mm][Pp](\s+\d+)?\s*$""".r
    val charNumberRE = """^\s*(\w)\s+(\d+)\s*$""".r
    val charNumberStringRE = """^\s*(\w)\s+(\d+)\s+(.*)$""".r

    def prompt() = print(">> ")
    def missingActorNumber() = println("Crash command requires an actor number.")
    def invalidInput(s: String) = println(s"Unrecognized command: $s")
    def invalidCommand(c: String): Unit = println(s"Expected 'c', 'r', 'u', or 'd'. Got $c")
    def invalidNumber(s: String): Unit = println(s"Expected a number. Got $s")
    def expectedString(): Unit = println("Expected a string after the command and number")
    def unexpectedString(c: String, n: Int): Unit = println(s"Extra arguments after command and number '$c $n'")
    def finished(): Nothing = exit("Goodbye!", 0)

    val handleLine: PartialFunction[String,Unit] = {
      case blankRE() => // do nothing
      case "h" | "help" => println(help)
      case dumpRE(n) => server ! (if (n == null) DumpAll else Dump(n.trim.toInt))
      case badCrashRE() => missingActorNumber()
      case crashRE(n) => server ! Crash(n.toInt)
      case charNumberStringRE(c, n, s) => c match {
        case "c" | "C" => server ! Create(n.toInt, s)
        case "u" | "U" => server ! Update(n.toInt, s)
        case "r" | "R" => unexpectedString(c, n.toInt)
        case "d" | "D" => unexpectedString(c, n.toInt)
        case _ => invalidCommand(c)
      }
      case charNumberRE(c, n) => c match {
        case "r" | "R" => server ! Read(n.toInt)
        case "d" | "D" => server ! Delete(n.toInt)
        case "c" | "C" => expectedString
        case "u" | "U" => expectedString
        case _ => invalidCommand(c)
      }
      case "q" | "quit" | "exit" => finished()
      case string => invalidInput(string)
    }

    while (true) {
      prompt()
      Console.in.readLine() match {
        case null => finished()
        case line => handleLine(line)
      }
    }
  }

  private val help = """Usage: AkkaClient [-h | --help]
    |Then, enter one of the following commands, one per line:
    |  h | help      Print this help message.
    |  c n string    Create "record" for key n for value string.
    |  r n           Read record for key n. It's an error if n isn't found.
    |  u n string    Update (or create) record for key n for value string.
    |  d n           Delete record for key n. It's an error if n isn't found.
    |  crash n       "Crash" worker n (to test recovery).
    |  dump [n]      Dump the state of all workers (default) or worker n.
    |  ^d | quit     Quit.
    |""".stripMargin

  private def exit(message: String, status: Int): Nothing = {
    for (sys <- system) sys.shutdown()
    println(message)
    sys.exit(status)
  }
}

2.3 配置文件

Akka 使用 Typesafe 的 Config 库进行配置,配置文件如下:

// src/main/resources/application.conf
akka {
  loggers  = [akka.event.slf4j.Slf4jLogger]
  loglevel = debug
  actor {
    debug {
      unhandled = on
      lifecycle = on
    }
  }
}
server {
  number-workers = 5
}

2.4 处理流程

以下是 AkkaClient 的处理流程:

graph TD;
    A[开始] --> B[处理命令行参数];
    B --> C[创建 ActorSystem];
    C --> D[创建 ServerActor];
    D --> E[确定工作者数量];
    E --> F[发送 Start 消息];
    F --> G[处理用户输入];
    G --> H{是否退出};
    H -- 否 --> G;
    H -- 是 --> I[关闭 ActorSystem];
    I --> J[退出程序];

2.5 代码解释

  • processArgs 方法用于处理命令行参数,支持 -h --help 选项。
  • processInput 方法使用正则表达式解析用户输入,并根据输入发送相应的消息给 ServerActor
  • help 变量包含详细的帮助信息。
  • exit 方法用于关闭 ActorSystem 并退出程序。

4. ServerActor 实现

// src/main/scala/progscala2/concurrency/akka/ServerActor.scala
package progscala2.concurrency.akka
import scala.util.{Try, Success, Failure}
import scala.util.control.NonFatal
import scala.concurrent.duration._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import akka.actor.{Actor, ActorLogging, ActorRef,
  ActorSystem, Props, OneForOneStrategy, SupervisorStrategy}
import akka.pattern.ask
import akka.util.Timeout

class ServerActor extends Actor with ActorLogging {
  import Messages._
  implicit val timeout = Timeout(1.seconds)

  override val supervisorStrategy: SupervisorStrategy = {
    val decider: SupervisorStrategy.Decider = {
      case WorkerActor.CrashException => SupervisorStrategy.Restart
      case NonFatal(ex) => SupervisorStrategy.Resume
    }
    OneForOneStrategy()(decider orElse super.supervisorStrategy.decider)
  }

  var workers = Vector.empty[ActorRef]

  def receive = initial

  val initial: Receive = {
    case Start(numberOfWorkers) =>
      workers = ((1 to numberOfWorkers) map makeWorker).toVector
      context become processRequests
  }

  val processRequests: Receive = {
    case c @ Crash(n) => workers(n % workers.size) ! c
    case DumpAll =>
      Future.fold(workers map (_ ? DumpAll))(Vector.empty[Any])(_ :+ _)
        .onComplete(askHandler("State of the workers"))
    case Dump(n) =>
      (workers(n % workers.size) ? DumpAll).map(Vector(_))
        .onComplete(askHandler(s"State of worker $n"))
    case request: Request =>
      val key = request.key.toInt
      val index = key % workers.size
      workers(index) ! request
    case Response(Success(message)) => printResult(message)
    case Response(Failure(ex)) => printResult(s"ERROR! $ex")
  }

  def askHandler(prefix: String): PartialFunction[Try[Any],Unit] = {
    case Success(suc) => suc match {
      case vect: Vector[_] =>
        printResult(s"$prefix:\n")
        vect foreach {
          case Response(Success(message)) =>
            printResult(s"$message")
          case Response(Failure(ex)) =>
            printResult(s"ERROR! Success received wrapping $ex")
        }
      case _ => printResult(s"BUG! Expected a vector, got $suc")
    }
    case Failure(ex) => printResult(s"ERROR! $ex")
  }

  protected def printResult(message: String) = {
    println(s"<< $message")
  }

  protected def makeWorker(i: Int) =
    context.actorOf(Props[WorkerActor], s"worker-$i")
}

object ServerActor {
  def make(system: ActorSystem): ActorRef =
    system.actorOf(Props[ServerActor], "server")
}

4.1 代码解释

  • 监督策略 supervisorStrategy 方法重写了默认的监督策略,使用一对一策略。如果发生模拟崩溃( WorkerActor.CrashException ),则重启演员;如果发生其他非致命异常,则继续执行。
  • 消息处理
    • initial 处理 Start 消息,创建工作者并切换到 processRequests 状态。
    • processRequests 处理其他消息,包括 Crash DumpAll Dump Request Response
  • 辅助方法
    • askHandler 处理 Future 的完成结果。
    • printResult 打印处理结果。
    • makeWorker 创建工作者演员。

4.2 ServerActor 处理流程

graph TD;
    A[接收到消息] --> B{是否为 Start 消息};
    B -- 是 --> C[创建工作者];
    C --> D[切换到 processRequests 状态];
    B -- 否 --> E{是否为 Crash 消息};
    E -- 是 --> F[发送 Crash 消息给对应工作者];
    E -- 否 --> G{是否为 DumpAll 消息};
    G -- 是 --> H[收集所有工作者状态];
    G -- 否 --> I{是否为 Dump 消息};
    I -- 是 --> J[收集指定工作者状态];
    I -- 否 --> K{是否为 Request 消息};
    K -- 是 --> L[转发请求给对应工作者];
    K -- 否 --> M{是否为 Response 消息};
    M -- 是 --> N[打印结果];
    M -- 否 --> O[未知消息处理];

5. WorkerActor 实现

// src/main/scala/progscala2/concurrency/akka/WorkerActor.scala
package progscala2.concurrency.akka
import scala.util.{Try, Success, Failure}
import akka.actor.{Actor, ActorLogging}

class WorkerActor extends Actor with ActorLogging {
  import Messages._
  private val datastore = collection.mutable.Map.empty[Long,String]

  def receive = {
    case Create(key, value) =>
      datastore += key -> value
      sender ! Response(Success(s"$key -> $value added"))
    case Read(key) =>
      sender ! Response(Try(s"${datastore(key)} found for key = $key"))
    case Update(key, value) =>
      datastore += key -> value
      sender ! Response(Success(s"$key -> $value updated"))
    case Delete(key) =>
      datastore -= key
      sender ! Response(Success(s"$key deleted"))
    case Crash(_) => throw WorkerActor.CrashException
    case DumpAll =>
      sender ! Response(Success(s"${self.path}: datastore = $datastore"))
  }
}

object WorkerActor {
  case object CrashException extends RuntimeException("Crash!")
}

5.1 代码解释

  • datastore 是一个可变的键值对映射,用于存储数据。
  • receive 方法处理不同的消息:
    • Create :添加新的键值对。
    • Read :尝试读取键对应的值。
    • Update :更新键对应的值。
    • Delete :删除键值对。
    • Crash :抛出 CrashException 模拟崩溃。
    • DumpAll :返回数据存储的状态。

5.2 WorkerActor 处理流程

graph TD;
    A[接收到消息] --> B{是否为 Create 消息};
    B -- 是 --> C[添加键值对];
    C --> D[发送成功响应];
    B -- 否 --> E{是否为 Read 消息};
    E -- 是 --> F[尝试读取值];
    F --> G[发送响应];
    E -- 否 --> H{是否为 Update 消息};
    H -- 是 --> I[更新键值对];
    I --> J[发送成功响应];
    H -- 否 --> K{是否为 Delete 消息};
    K -- 是 --> L[删除键值对];
    L --> M[发送成功响应];
    K -- 否 --> N{是否为 Crash 消息};
    N -- 是 --> O[抛出异常];
    N -- 否 --> P{是否为 DumpAll 消息};
    P -- 是 --> Q[发送数据存储状态];
    P -- 否 --> R[未知消息处理];

6. 运行示例

6.1 运行命令

在 sbt 提示符下运行:

run-main progscala2.concurrency.akka.AkkaClient

或者使用 run 并从列表中选择。输入 h 查看命令列表并尝试几个命令,使用 quit 退出。

也可以使用以下命令从 shell 或命令窗口运行命令文件:

sbt "run-main progscala2.concurrency.akka.AkkaClient" < misc/run-akka-input.txt

6.2 注意事项

  • 由于操作本质上是异步的,每次运行脚本或复制粘贴输入行时会看到不同的结果。
  • 当演员崩溃时,数据会丢失。如果这不可接受,可以使用 Akka Persistence 模块进行持久化。
  • ServerActor 通过 ActorRef 访问演员,确保即使演员崩溃,引用仍然有效。

通过上述示例,我们可以看到 Akka 演员模型在处理并发和错误处理方面的强大功能,能够有效地管理系统状态和处理异常情况。

【2025年10月最新优化算法】混沌增强领导者黏菌算法(Matlab代码实现)内容概要:本文档介绍了2025年10月最新提出的混沌增强领导者黏菌算法(Matlab代码实现),属于智能优化算法领域的一项前沿研究。该算法结合混沌机制与黏菌优化算法,通过引入领导者策略提升搜索效率和全局寻优能力,适用于复杂工程优化问题的求解。文档不仅提供完整的Matlab实现代码,还涵盖了算法原理、性能验证及与其他优化算法的对比分析,体现了较强的科研复现性和应用拓展性。此外,文中列举了大量相关科研方向和技术应用场景,展示其在微电网调度、路径规划、图像处理、信号分析、电力系统优化等多个领域的广泛应用潜力。; 适合人群:具备一定编程基础和优化理论知识,从事科研工作的研究生、博士生及高校教师,尤其是关注智能优化算法及其在工程领域应用的研发人员;熟悉Matlab编程环境者更佳。; 使用场景及目标:①用于解决复杂的连续空间优化问题,如函数优化、参数辨识、工程设计等;②作为新型元启发式算法的学习与教学案例;③支持高水平论文复现与算法改进创新,推动在微电网、无人机路径规划、电力系统等实际系统中的集成应用; 其他说明:资源包含完整Matlab代码和复现指导,建议结合具体应用场景进行调试与拓展,鼓励在此基础上开展算法融合与性能优化研究。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值