Объясни акторную модель (на примере Akka)
Акторная модель (actor model) — это модель конкурентных вычислений, в которой всё взаимодействие между сущностями происходит через передачу сообщений. Вместо того чтобы разделять память и использовать потоки с синхронизацией, как в классической многопоточности, акторная модель изолирует состояние и оперирует сообщениями между независимыми "акторными" единицами. Эта модель идеально подходит для построения распределённых, масштабируемых и отказоустойчивых систем.
Основы акторной модели
-
Актор (actor) — это единица, которая:
-
получает сообщения,
-
может изменить своё внутреннее состояние,
-
может отправить сообщение другим акторам,
-
может создать новых акторов.
-
-
Изоляция состояния: каждый актор имеет собственное состояние, к которому нельзя получить прямой доступ извне. Взаимодействие идёт только через обмен сообщениями.
-
Асинхронное взаимодействие: сообщения отправляются асинхронно и обрабатываются в порядке получения.
-
Почтовый ящик (mailbox): каждый актор имеет очередь сообщений, которую он обрабатывает последовательно (один за другим).
Akka — реализация акторной модели на JVM
Akka — это популярный фреймворк на Scala (и Java), реализующий акторную модель. Он построен на основе идей Erlang, но для платформы JVM. В Akka каждый актор — это экземпляр класса, который обрабатывает входящие сообщения.
Создание и работа с акторами в Akka
1. Объявление актора
import akka.actor._
class Printer extends Actor {
def receive: Receive = {
case message: String => println(s"Received: $message")
}
}
Метод receive определяет, как актор обрабатывает полученные сообщения.
2. Запуск актора
val system = ActorSystem("MySystem")
val printer = system.actorOf(Props\[Printer\], "printer")
-
ActorSystem — контейнер для всех акторов.
-
Props — фабрика, описывающая, как создать актора.
3. Отправка сообщения
printer ! "Hello, actor"
Оператор ! (называется "tell") отправляет сообщение актору асинхронно. Актор поместит его в свой почтовый ящик и обработает позже.
Поведение актора
В Akka можно менять поведение актора во время выполнения с помощью context.become:
class Switch extends Actor {
def on: Receive = {
case "off" =>
println("Switching off")
context.become(off)
}
def off: Receive = {
case "on" =>
println("Switching on")
context.become(on)
}
def receive = off
}
Это позволяет реализовывать конечные автоматы и менять реакцию на сообщения в зависимости от текущего состояния.
Обработка ошибок
Akka реализует иерархию надзора (supervision tree), где каждый актор может следить за дочерними акторами:
class Supervisor extends Actor {
override val supervisorStrategy =
OneForOneStrategy() {
case \_: ArithmeticException => Restart
}
def receive = {
case props: Props =>
val child = context.actorOf(props)
sender() ! child
}
}
Если дочерний актор падает, родитель может его перезапустить, остановить, возобновить или эскалировать ошибку. Это даёт мощный инструмент для устойчивости системы.
Анонимные и временные акторы
Akka позволяет создавать анонимные акторы, особенно удобно в случае использования ask-паттерна (?), который позволяет получить Future в ответ:
import akka.pattern.ask
import akka.util.Timeout
import scala.concurrent.duration._
implicit val timeout = Timeout(5.seconds)
val future = printer ? "Ping"
В этом случае создаётся временный актор, который ждёт ответ от целевого актора.
Распределённость
Akka может работать в кластере, где акторами управляют узлы на разных машинах. В этом случае сообщения пересылаются по сети прозрачно для разработчика. Каждый актор имеет уникальный адрес: akka://MySystem@127.0.0.1:2552/user/worker1.
Плюсы акторной модели в Akka
-
Изоляция состояния — минимизирует гонки данных.
-
Асинхронность по умолчанию — улучшает масштабируемость.
-
Fault tolerance — благодаря supervision стратегиям.
-
Простота распараллеливания — каждый актор работает независимо.
-
Распределённость — масштабирование на уровне кластера.
Почтовый ящик и однопоточность
Каждый актор обрабатывает сообщения по одному, в порядке получения. Это гарантирует, что состояния актора никогда не читаются или не изменяются из разных потоков одновременно. Даже если тысячи сообщений приходят одновременно, они будут поставлены в очередь и обрабатываться последовательно.
Жизненный цикл актора
-
создание — context.actorOf
-
инициализация — preStart()
-
нормальная работа — receive
-
остановка — context.stop(self)
-
удаление — postStop()
Иерархия акторов
Все акторы создаются внутри ActorSystem. Они могут создавать дочерние акторы. Иерархия выглядит как дерево, где каждый родитель наблюдает за своими детьми. Это облегчает управление зависимостями и контроль ошибок.
Расширенные возможности
-
Stash — временное откладывание сообщений.
-
Timers — для реализации таймаутов и периодических задач.
-
Persistence — для создания надёжных и восстанавливаемых акторов.
-
Typed Actors (Akka Typed) — типобезопасный подход к работе с акторами, разработан в последних версиях Akka.
Пример: простой чат
class User(name: String) extends Actor {
def receive = {
case msg: String => println(s"$name received: $msg")
}
}
val alice = system.actorOf(Props(new User("Alice")))
val bob = system.actorOf(Props(new User("Bob")))
alice.tell("Hi Bob", bob) // сообщение от Алисы Бобу
Каждый актор можно рассматривать как отдельного пользователя, взаимодействующего через обмен сообщениями.