Акторы и их жизненнный цикл - I
Анатомия актора
Как известно, модель акторов является высокоуровневой абстракцией для создания параллельных приложений, освобождающая разработчика от необходимости явно управлять блокировками и потоками. Как показано ниже, актор состоит из нескольких взаимодействующих элементов. ActorRef
представляет собой логический адрес актора и позволяет нам асинхронно посылать сообщения актору, т.е. не дожидаясь отклика. Dispatcher (диспетчер) – по умолчанию обычно есть 1 dispatcher на actor system – отвечает за постановку сообщений в очередь в mailbox’е актора, а также за scheduling извлечения сообщений из mailbox’а – причем только по одному за раз – для их последующей обработки в акторе. И наконец, актор, реализующий трейт Actor
(собственно, это весь API, который мы должны реализовать) – инкапсулирует и состояние, и поведение.
Как мы увидим позднее, Akka не дает нам прямого доступа к Actor’у и таким образом обеспечивает то, что отправка асинхронных сообщений - единственный способ взаимодействия с актором: невозможно вызвать метод у актора. Также стоит заметить что отсылка сообщения актору и обработка этого сообщения актором - два совершенно разных действия, которые к тому же выполняются в разных потоках – и разумеется, Akka обеспечивает всю необходимую синхронизацию целью защиты состояния актора. Следовательно, Akka как бы создает иллюзию однопоточности, т.е. нам не нужно заботится о синхронизации доступа к разделяемой памяти.
Реализация актора
В Akka актор - класс, который реализует трейт Actor
:
class SimplestActor extends Actor {
override def receive = Actor.emptyBehavior
}
Метод receive
возвращает т.н. “исходное поведение” актора. Каждый раз, когда нужно обработать сообщение, для сообщения ищется соотвествующий обработчик в текущем поведении актора. Поведение актора - функция, которая содержит ряд действий, которые нужно совершить в ответ на некоторые сообщения, например, адресовать запрос далее, если авторизация пройдена, или отклонить запрос в другом случае. Данное поведение может со временем измениться - например, клиенты, от которых приходят запросы, постепенно проходят авторизацию, или акторы могут входить в сотояние временной неработоспособности, а позднее опять начать обработку запросов. Эти изменения отражаются в переменных состояния; также сама функция может быть динамически заменена на другую - см. операции become
и unbecome
. Однако, исходное поведение, определенное в ходе создания актора, является специфическим в том смысле, что при перезапуске оно будет снова установлено для актора. В примере приведен простейший актор, не обрабатывающий никаких сообщений, т.к. частичная функцияActor.emptyBehavior
не определена ни для одного значения.
В смысле реализации это просто частично определенная функция, вызываемая Аккой для обработки сообщений. Поскольку поведением является PartialFunction[Any, Unit]
, по умолчанию невозможно определить актор, который принимает сообщения определеного типа. Существует, правда, модуль akka-typed
, назначение которого - добавить безопасность типов в Akka, но он является все еще экспериментальным.
Системы акторов и создание акторов
По выражению автора модели акторов Карла Хьюитта, “один актор не является актором, акторы работают в системе”. В Akka система акторов является совокупностью взаимодействующих акторов, из которых формируется иерархия - т.е. каждый актор имеет актора-родителя.
Когда мы создаем систему акторов, Akka создает сразу как минимум 3 актора: т.н. root guardian, который является корневым актором всей иерархии, а также еще два актора - user guardian и system guardian. user guardian – который часто просто называют guardian – является родителем всех акторов верхнего уровня, создаваемых в приложении – т.е. в данном случае “верхний уровень” означает “самый верхний из доступных приложению”.
Чтобы создать систему акторов, нужно вызывать factory-метод объекта ActorSystem:
val system = ActorSystem("our-actor-system")
И без создания ActorSystem
мы не сможем создавать акторы - поскольку просто вызов конструктора Actor
приведет к exception’у.
throw ActorInitializationException(
s"You cannot create an instance of [${getClass.getName}] explicitly using the constructor (new). " +
"You have to use one of the 'actorOf' factory methods to create a new actor. See the documentation.")
В сообщении exception’а все сказано – мы должны использовать factory-метод из ActorSystem, который назвается actorOf
:
system.actorOf(Props[PublishSubscribeActor], "publish-subscribe-actor")
actorOf
не возвращает непосредственно экземпляр Actor
, а экземпляр ActorRef
, так что клиентский код не имеет прямого доступа к Actor
, в том числе для того чтобы обеспечить то, что асинхронные сообщения - единственный вариант взаимодействия с актором. Имя актора должно быть уникальным на данном уровне иерархии акторов - или опять-таки будет выброшен exception. Если не указать имя, то Akka
все равно сгенерирует его автоматически, так что все акторы имеют имя.
Что такое Props
? Это конфигурационный объект актора. Он параметризуется класcом актора или принимает его на вход (ну или принимает на вход closure, создающий актор, например, вызов конструктора, но этот метод по определенным причинам не рекомендуется использовать). Рекомендуемой практикой является создание factory-метода, который создает необходимый Props
в companion object’е актора.
Приведем пример простейшего актора - PublishSubscribeActor
, вместе с заготовкой теста для него:
object PublishSubscribeActor {
final val Name = "publish-subscribe-actor"
def props: Props = Props[PublishSubscribeActor]
}
class PublishSubscribeActor extends Actor {
override def receive = Actor.emptyBehavior
}
class PublishSubscribeActorSpec extends TestKit(ActorSystem("publish-subscibe-actor-system"))
with WordSpecLike
with Matchers
with BeforeAndAfterAll {
"A PublishSubscribeActor" in {
EventFilter.debug(occurrences = 1, pattern = s"started.*${classOf[PublishSubscribeActor].getName}").intercept {
system.actorOf(PublishSubscribeActor.props)
}
}
override protected def afterAll() = {
system.shutdown()
}
}