«1. Введение

Akka — это библиотека с открытым исходным кодом, которая помогает легко разрабатывать параллельные и распределенные приложения с использованием Java или Scala, используя акторную модель.

В этом уроке мы представим основные функции, такие как определение актеров, как они общаются и как мы можем их убить. В заключительных заметках мы также отметим некоторые рекомендации по работе с Akka.

2. Модель акторов

Модель акторов не нова для компьютерного сообщества. Впервые он был представлен Карлом Эдди Хьюиттом в 1973 году как теоретическая модель для обработки параллельных вычислений.

Он начал демонстрировать свою практическую применимость, когда индустрия программного обеспечения начала осознавать подводные камни реализации параллельных и распределенных приложений.

Актор представляет собой независимую вычислительную единицу. Вот некоторые важные характеристики:

    актор инкапсулирует свое состояние и часть логики приложения, акторы взаимодействуют только посредством асинхронных сообщений, а не через прямые вызовы методов, каждый актор имеет уникальный адрес и почтовый ящик, в который другие акторы могут доставлять сообщения, которые будет обрабатывать все сообщения в почтовом ящике в последовательном порядке (реализацией почтового ящика по умолчанию является очередь FIFO) система акторов организована в виде древовидной иерархии актор может создавать других акторов, может отправлять сообщения любому другому актору и останавливаться сам или любой актор создал

2.1. Преимущества

Разработка параллельных приложений сложна, потому что нам нужно иметь дело с синхронизацией, блокировками и общей памятью. Используя акторы Akka, мы можем легко писать асинхронный код без необходимости блокировки и синхронизации.

Одним из преимуществ использования сообщения вместо вызова метода является то, что поток-отправитель не будет блокироваться в ожидании возвращаемого значения при отправке сообщения другому актору. Принимающий актор ответит с результатом, отправив ответное сообщение отправителю.

Еще одно большое преимущество использования сообщений заключается в том, что нам не нужно беспокоиться о синхронизации в многопоточной среде. Это связано с тем, что все сообщения обрабатываются последовательно.

Еще одним преимуществом модели акторов Akka является обработка ошибок. Путем организации субъектов в иерархию каждый субъект может уведомить своего родителя о сбое, чтобы он мог действовать соответствующим образом. Родительский актор может принять решение об остановке или перезапуске дочерних акторов.

3. Настройка

Чтобы воспользоваться преимуществами актеров Akka, нам нужно добавить следующую зависимость от Maven Central:

<dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-actor_2.12</artifactId>
    <version>2.5.11</version>
</dependency>

4. Создание актера

Как уже упоминалось, актеры определяются в иерархическая система. Все акторы, которые имеют общую конфигурацию, будут определены в ActorSystem.

Сейчас мы просто определим ActorSystem с конфигурацией по умолчанию и произвольным именем:

ActorSystem system = ActorSystem.create("test-system");

Несмотря на то, что мы еще не создали ни одного актора, система уже будет содержать 3 основных актора: ~~ ~ корневой актор-хранитель, имеющий адрес «/», который, как указано в названии, представляет собой корень системной иерархии акторов, а пользовательский актор-хранитель имеет адрес «/user». Это будет родителем всех актеров, которых мы определяем как системных опекунов, имеющих адрес «/system». Это будет родителем для всех акторов, определенных внутри системы Akka.

    Любой актор Akka будет расширять абстрактный класс AbstractActor и реализовывать метод createReceive() для обработки входящих сообщений от других акторов:

Это это самый простой актер, которого мы можем создать. Он может получать сообщения от других акторов и отбрасывать их, поскольку в ReceiveBuilder не определены соответствующие шаблоны сообщений. Мы поговорим о сопоставлении шаблонов сообщений позже в этой статье.

public class MyActor extends AbstractActor {
    public Receive createReceive() {
        return receiveBuilder().build();
    }
}

Теперь, когда мы создали нашего первого актора, мы должны включить его в ActorSystem:

4.1. Конфигурация актера

ActorRef readingActorRef 
  = system.actorOf(Props.create(MyActor.class), "my-actor");

Класс Props содержит конфигурацию актера. Мы можем настроить такие вещи, как диспетчер, почтовый ящик или конфигурацию развертывания. Этот класс является неизменяемым, поэтому потокобезопасен, поэтому его можно использовать совместно при создании новых акторов.

«Настоятельно рекомендуется и считается лучшей практикой определять фабричные методы внутри объекта актора, которые будут обрабатывать создание объекта Props.

В качестве примера давайте определим актера, который будет выполнять некоторую обработку текста. Актор получит объект String, над которым он будет выполнять обработку:

Теперь, чтобы создать экземпляр этого типа актора, мы просто используем фабричный метод props() для передачи аргумента String конструктору. :

public class ReadingActor extends AbstractActor {
    private String text;

    public static Props props(String text) {
        return Props.create(ReadingActor.class, text);
    }
    // ...
}

Теперь, когда мы знаем, как определить актора, давайте посмотрим, как они взаимодействуют внутри системы акторов.

ActorRef readingActorRef = system.actorOf(
  ReadingActor.props(TEXT), "readingActor");

5. Обмен сообщениями акторов

Для взаимодействия друг с другом акторы могут отправлять и получать сообщения от любого другого актора в системе. Эти сообщения могут быть объектами любого типа с условием, что они неизменяемы.

Лучше всего определять сообщения внутри класса актера. Это помогает писать код, который легко понять и знать, какие сообщения может обрабатывать актор.

5.1. Отправка сообщений

Внутри системы акторов Akka сообщения отправляются с использованием методов:

tell() ask() forward()

    Когда мы хотим отправить сообщение и не ожидаем ответа, мы можем использовать метод метод рассказать(). Это наиболее эффективный метод с точки зрения производительности:

Первый параметр представляет собой сообщение, которое мы отправляем по адресу актора readActorRef.

readingActorRef.tell(new ReadingActor.ReadLines(), ActorRef.noSender());

Второй параметр указывает, кто является отправителем. Это полезно, когда актор, получающий сообщение, должен отправить ответ актору, отличному от отправителя (например, родителю отправляющего актора).

Обычно мы можем установить второй параметр равным null или ActorRef.noSender(), потому что мы не ожидаем ответа. Когда нам нужен ответ от актора, мы можем использовать метод ask():

При запросе ответа от актора возвращается объект CompletionStage, поэтому обработка остается неблокирующей.

CompletableFuture<Object> future = ask(wordCounterActorRef, 
  new WordCounterActor.CountWords(line), 1000).toCompletableFuture();

Очень важный факт, на который мы должны обратить внимание, это обработка ошибок внутри актора, который будет реагировать. Чтобы вернуть объект Future, который будет содержать исключение, мы должны отправить сообщение Status.Failure актору-отправителю.

Это не делается автоматически, когда актор генерирует исключение при обработке сообщения, и вызов ask() истечет по тайм-ауту, и в логах не будет видно ссылки на исключение:

У нас также есть форвард (), который похож на tell(). Разница в том, что первоначальный отправитель сообщения сохраняется при отправке сообщения, поэтому актор, пересылающий сообщение, действует только как промежуточный актор:

@Override
public Receive createReceive() {
    return receiveBuilder()
      .match(CountWords.class, r -> {
          try {
              int numberOfWords = countWordsFromLine(r.line);
              getSender().tell(numberOfWords, getSelf());
          } catch (Exception ex) {
              getSender().tell(
               new akka.actor.Status.Failure(ex), getSelf());
               throw ex;
          }
    }).build();
}

5.2. Получение сообщений

printerActorRef.forward(
  new PrinterActor.PrintFinalResult(totalNumberOfWords), getContext());

Каждый актор реализует метод createReceive(), который обрабатывает все входящие сообщения. ReceiveBuilder() действует как оператор switch, пытаясь сопоставить полученное сообщение с определенным типом сообщений:

При получении сообщение помещается в очередь FIFO, поэтому сообщения обрабатываются последовательно.

public Receive createReceive() {
    return receiveBuilder().matchEquals("printit", p -> {
        System.out.println("The address of this actor is: " + getSelf());
    }).build();
}

6. Уничтожение актера

Когда мы закончили использовать актера, мы можем остановить его, вызвав метод stop() из интерфейса ActorRefFactory:

Мы можем использовать этот метод для завершения любого дочернего актера или сам актер. Важно отметить, что остановка выполняется асинхронно и что текущая обработка сообщения завершится до того, как актор будет завершен. Входящие сообщения больше не будут приниматься в почтовый ящик актера.

system.stop(myActorRef);

Остановив родительский актор, мы также отправим сигнал уничтожения всем дочерним акторам, которые были им порождены.

Когда нам больше не нужна система акторов, мы можем завершить ее, чтобы высвободить все ресурсы и предотвратить утечку памяти: это система Акка.

Мы также можем отправить сообщение PoisonPill любому актеру, которого мы хотим убить:

Future<Terminated> terminateResponse = system.terminate();

Сообщение PoisonPill будет получено актером, как и любое другое сообщение, и помещено в очередь. Актер будет обрабатывать все сообщения, пока не дойдет до сообщения PoisonPill. Только после этого актор начнет процесс прекращения.

«Еще одним специальным сообщением, используемым для убийства актера, является сообщение Kill. В отличие от PoisonPill, при обработке этого сообщения актор выдаст исключение ActorKilledException:

myActorRef.tell(PoisonPill.getInstance(), ActorRef.noSender());

7. Заключение

В этой статье мы представили основы фреймворка Akka. Мы показали, как определять акторов, как они общаются друг с другом и как их завершать.

myActorRef.tell(Kill.getInstance(), ActorRef.noSender());

В заключение мы остановимся на некоторых рекомендациях по работе с Akka:

используйте tell() вместо ask(), когда производительность является проблемой; при использовании ask() мы всегда должны обрабатывать исключения, отправляя сообщение о сбое. не иметь общего изменяемого состояния; актор не должен быть объявлен в другом акторе; актор не останавливается автоматически, когда на него больше не ссылаются. Мы должны явно уничтожить актера, когда он нам больше не нужен, чтобы предотвратить утечку памяти. Сообщения, используемые акторами, всегда должны быть неизменяемыми

Как всегда, исходный код статьи доступен на GitHub.

    »

As always, the source code for the article is available over on GitHub.