Содержание


Одновременное исполнение на платформе JVM

Асинхронное исполнение с помощью Akka

Построение систем акторов для параллельных приложений

Comments

Серия контента:

Этот контент является частью # из серии # статей: Одновременное исполнение на платформе JVM

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Одновременное исполнение на платформе JVM

Следите за выходом новых статей этой серии.

В предшествующих статьях этого цикла реализация параллелизма рассматривалась в следующих ракурсах.

  • Параллельное исполнение одной и той же операции с несколькими наборами данных (как в случае потоков Java 8)
  • Явное структурирование вычислений таким образом, чтобы выполнять определенные операции асинхронно, а затем объединять результаты (как в случае future)

Оба эти способа прекрасно подходят для достижения параллелизма, однако их необходимо в явном реализовать при проектировании приложения.

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

Базовые сведения о модели акторов

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

В терминологии Akka акторы выглядят как несколько невротические пакеты поведений, взаимодействующие посредством сообщений. Подобно настоящим актерам, акторы Akka нуждаются в определенной степени конфиденциальности. Невозможно отправлять сообщения непосредственно актору Akka. Вместо этого сообщения отправляются на ссылку актора, которая представляет собой эквивалент почтового ящика. Входящие сообщения маршрутизируются посредством ссылки в почтовый ящик актора и только после этого доставляются актору. Более того, акторы Akka требуют, чтобы все входящие сообщения были стерильными (в терминологии JVM - иммутабельными), чтобы исключить загрязнения от других акторов.

В отличие от требований некоторых реальных актеров, эти, на первый взгляд маниакальные, ограничения в Akka имеют под собой определенную основу. Использование ссылок для акторов предотвращает любые взаимодействия (помимо обмена сообщениями), способные нарушить разъединение в ядре модели акторов. Акторы исполняются в однопоточном режиме (в конкретном экземпляре актора никогда не исполняется более одного потока), поэтому почтовый ящик действует как буфер для размещения входящих сообщений до того момента, когда появится возможность для их обработки. И, наконец, иммутабельность сообщений (прямо заявленная в качестве требования, но в настоящее время не применяемая в Akka принудительно вследствие ограничений JVM) полностью освобождает разработчика от проблем синхронизации, которые могли бы влиять на данные, совместно используемые акторами: если все совместно используемые данные являются иммутабельными, то необходимость в синхронизации никогда не возникает.

Простая программа hello

Теперь, после краткого обзора модели акторов и специфических особенностей Akka, перейдем к рассмотрению конкретного программного кода. В примере написания программного кода мы используем простую программу hello. Это клише позволяет быстро получить простой в понимании образ языка или системы. В листинге 1 показана версия Akka в среде Scala.

Листинг 1. Простая программа hello на Scala
 import akka.actor._ import akka.util._ /** Простая Scala-программа hello на основе актора. */ object Hello1 extends App { val system = ActorSystem("actor-demo-scala") val hello = system.actorOf(Props[Hello]) hello ! "Bob" Thread sleep 1000 system shutdown class Hello extends Actor { def receive = { case name: String => println(s"Hello $name") } } }

Код в листинге 1 находится в двух отдельных фрагментах, содержащихся внутри объекта приложения Hello1. Первый фрагмент кода образует инфраструктуру Akka-приложения, которая делает следующее:

  1. Создает систему акторов (строка ActorSystem(...)).
  2. Создает внутри этой системы актор (строка system.actorOf(...), который возвращает ссылку актора для созданного актора).
  3. Использует ссылку актора для отправки сообщения актору (строка hello ! "Bob").
  4. Ждет одну секунду, а затем завершает работу системы акторов (строка system shutdown).

Вызов system.actorOf(Props[Hello]) – это рекомендуемый способ для создания экземпляра актора с использованием свойств конфигурации, специализированных для типа актора Hello. Для этого простого актора (играющего эпизодическую роль с диалогом в одну строку) нет никакой конфигурационной информации, соответственно объект Props не имеет параметров. Если разработчик хочет настроить конфигурацию на своем акторе, он может специально для этого актора определить класс Props, содержащий всю необходимую информацию (в последующем примере показано, как это сделать).

Оператор hello ! "Bob" отправляет сообщение (в данном случае это строка Bob) в созданный актор. Оператор ! предлагает удобный способ для представления отправки сообщения актору в Akka по принципу "отправил и забыл" (fire-forget). Если вам не нравится стиль этого специализированного оператора, то же самое можно сделать с помощью метода tell().

Второй фрагмент кода – это определение актора Hello, начинающееся с оператора class Hello extends Actor. Данное конкретное определение актора является предельно простым. Оно определяет обязательную (для всех акторов) частичную функцию receive, которая реализует обработку входящих сообщений (receive – это частичная функция, поскольку она определена только для некоторых входных данных — в нашем случае только для входных сообщений типа String). Обработка, реализованная для этого актора, состоит в выводе приветствия с использованием значения сообщения при каждом получении сообщения типа String.

Hello на языке Java

В листинге 2 представлен Akka-код Hello (см. листинг 1) на обычном языке Java.

Листинг 2. Hello на языке Java
 import akka.actor.*; public class Hello1 { public static void main(String[] args) { ActorSystem system = ActorSystem.create("actor-demo-java"); ActorRef hello = system.actorOf(Props.create(Hello.class)); hello.tell("Bob", ActorRef.noSender()); try { Thread.sleep(1000); } catch (InterruptedException e) { /* ignore */ } system.shutdown(); } private static class Hello extends UntypedActor { public void onReceive(Object message) throws Exception { if (message instanceof String) { System.out.println("Hello " + message); } } } }

В листинге 3 показано определение актора для версии Java 8 с лямбда-выражениями, вместе с необходимым импортом для класса ReceiveBuilder, поддерживающего лямбда-выражения. Наверное, листинг 3 несколько компактнее, но в остальном он почти такой же, как листинг 2.

Листинг 3. Версия Hello на языке Java 8
 import akka.japi.pf.ReceiveBuilder; ... private static class Hello extends AbstractActor { public Hello() { receive(ReceiveBuilder. match(String.class, s -> { System.out.println("Hello " + s); }). build()); } }

В отличие от кода в листинге 2, код на языке Java 8 в листинге 3 использует другой базовый класс —AbstractActor вместо UntypedActor— а также использует другой способ определения альтернатив для обработки сообщений. Класс ReceiveBuilder cпозволяет использовать лямбда-выражения для определения обработки сообщений, с синтаксисом сопоставления, несколько напоминающим Scala. Если вы ведете разработку преимущественно на языке Scala, то этот прием поможет вашему коду Java Akka выглядеть немного чище, однако в остальных случаях преимущества использования специальной версии для Java 8 выглядят весьма незначительными.

Почему нужно ждать?

Код основного приложения включает ожидание в форме Thread sleep 1000 (от отправки сообщения актору и до завершения работы системы). Может возникнуть вопрос, почему это необходимо. В конечном итоге обработка сообщения является тривиальной; вроде бы можно перейти к актору немедленно и обработать его в тот момент, когда утверждение hello ! "Bob" выполнено?

Краткий ответ на этот вопрос – "нет". Акторы Akka исполняются асинхронно, поэтому даже если целевой актор находится внутри той же JVM-машины, что и отправитель, исполнение целевого актора никогда не происходит немедленно. Вместо этого поток, осуществляющий отсылку сообщения, добавляет сообщение в почтовый ящик целевого актора. В свою очередь, добавление сообщения в почтовый ящик инициирует исполнение потока, который извлекает это сообщение из почтового ящика и обрабатывает его посредством вызова метода receive актора. Однако в общем случае извлечение сообщения из почтового ящика осуществляет другой поток, а не тот же поток, который добавил это сообщение в почтовый ящик.

Синхронизация доставки сообщений и гарантии

Краткий ответ на вопрос "Почему нужно ждать?" опирается на более глубокий принцип. Akka поддерживает дистанционную работу акторов с прозрачностью по местоположению. Это значит, что код разработчика не имеет прямого способа узнать, находится ли определенный актор внутри той же JVM-машины или исполняется в какой-либо системе где-то в облаке. Очевидно, что реальное функционирование в этих двух случаях будет существенно различаться.

Akka не гарантирует доставку сообщений. Философское объяснение отсутствия гарантии доставки восходит к одному из основополагающих принципов Akka

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

Akka гарантирует, что сообщения будут доставлены не более одного раза и что сообщения, отправленные из одного экземпляра актора в другой экземпляр актора, всегда будут получены в том же порядке. Вторая часть этой гарантии применима только к определенным парам акторов и не является ассоциативной. Если актор A отправляет сообщения актору B, то порядок получения этих сообщений никогда не будет нарушен. Такая же ситуация имеет место, если актор A отправляет сообщения актору C. Но если актор B также отправляет сообщения актору C (например, посредством переадресации сообщения от актора A к актору C), то сообщения актора B могут поступать в измененном порядке относительно сообщений от актора A.

В коде, показанном в листинге 1, вероятность потери сообщения довольно низка, поскольку этот код исполняется в единственной JVM-машине и не генерирует значительной нагрузки по передаче сообщений (высокая нагрузка по передаче сообщений может привести к потере сообщения; например, если у Akka исчерпалось пространство для хранения сообщений, отбрасывание сообщений неизбежно). Однако код в листинге 1 структурирован таким образом, чтобы не делать никаких допущений о синхронизации доставки сообщений и допускать асинхронное функционирование системы акторов.

Акторы и состояние

Модель акторов Akka является достаточно гибкой и допускает любые типы акторов. Можно использовать акторы без информации о состоянии (как в примере Hello1), однако такой актор скорее является эквивалентом вызовов метода. Добавление информации о состоянии позволяет существенно повысить гибкость функций акторов.

В листинге 1 приведен полный (хотя и тривиальный) пример системы акторов — однако в этом примере актор ограничен таким образом, чтобы всегда делать в точности одно и то же, снова и снова. Даже акторам надоедает повторять одну и ту же строку, поэтому код в листинге 4 сделан несколько более интересным посредством добавления к актору некоторой информации о состоянии.

Листинг 4. Hello в Scala на разных языках
 object Hello2 extends App { case class Greeting(greet: String) case class Greet(name: String) val system = ActorSystem("actor-demo-scala") val hello = system.actorOf(Props[Hello], "hello") hello ! Greeting("Hello") hello ! Greet("Bob") hello ! Greet("Alice") hello ! Greeting("Hola") hello ! Greet("Alice") hello ! Greet("Bob") Thread sleep 1000 system shutdown class Hello extends Actor { var greeting = "" def receive = { case Greeting(greet) => greeting = greet case Greet(name) => println(s"$greeting $name") } } }

Актор в листинге 4 умеет обрабатывать два различных типа сообщений, определенных в начале листинга: сообщение Greeting и сообщение Greet, каждое из которых обертывает строковое значение. Когда измененный актор Hello получает сообщение Greeting, он сохраняет обернутую строку как значение greeting. Когда этот актор получает сообщение Greet, он объединяет это сохраненное значение greeting со строкой Greet, формируя полное сообщение. При исполнении этого приложения на консоль будет выведена следующая информация (хотя и не обязательно в указанном порядке, поскольку порядок исполнения актора не является детерминированным).

 Hello Bob Hello Alice Hola Alice Hola Bob

В коде в листинге 4 не слишком много нового, поэтому я не показал здесь Java-версии. Эти версии включены в загружаемый код для этой статьи (см. раздел Ресурсы) в виде файловcom.sosnoski.concur.article5java.Hello2 и com.sosnoski.concur.article5java8.Hello2.

Свойства и взаимодействия

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

Листинг 5. Свойства и взаимодействия акторов
 object Hello3 extends App { import Greeter._ val system = ActorSystem("actor-demo-scala") val bob = system.actorOf(props("Bob", "Howya doing")) val alice = system.actorOf(props("Alice", "Happy to meet you")) bob ! Greet(alice) alice ! Greet(bob) Thread sleep 1000 system shutdown object Greeter { case class Greet(peer: ActorRef) case object AskName case class TellName(name: String) def props(name: String, greeting: String) = Props(new Greeter(name, greeting)) } class Greeter(myName: String, greeting: String) extends Actor { import Greeter._ def receive = { case Greet(peer) => peer ! AskName case AskName => sender ! TellName(myName) case TellName(name) => println(s"$greeting, $name") } } }

В листинге 5 демонстрируется новый актор Greeter, играющий ведущую роль. По сравнению с примером Hello2 в акторе Greeter дополнительно выполняются следующие действия.

  • Осуществляется передача свойств для конфигурирования экземпляров Greeter.
  • Используется сопутствующий Scala-объект, который определяет конфигурационные свойства и сообщения (в терминологии Java сопутствующий объект можно рассматривать как статический helper-класс с таким же именем, как класс актора.
  • Между экземплярами актора Greeter пересылаются сообщения.

Результатом работы этого кода является следующий простой текст.

 Howya doing, Alice Happy to meet you, Bob

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

Актор Greeter в Java

В листинге 6 показан код Akka-актора Greeter (см. листинг 5) в виде обычного Java-кода.

Листинг 6. Актор Greeter в Java
 public class Hello3 { public static void main(String[] args) { ActorSystem system = ActorSystem.create("actor-demo-java"); ActorRef bob = system.actorOf(Greeter.props("Bob", "Howya doing")); ActorRef alice = system.actorOf(Greeter.props("Alice", "Happy to meet you")); bob.tell(new Greet(alice), ActorRef.noSender()); alice.tell(new Greet(bob), ActorRef.noSender()); try { Thread.sleep(1000); } catch (InterruptedException e) { /* ignore */ } system.shutdown(); } // сообщения private static class Greet { public final ActorRef target; public Greet(ActorRef actor) { target = actor; } } private static Object AskName = new Object(); private static class TellName { public final String name; public TellName(String name) { this.name = name; } } // реализация актора private static class Greeter extends UntypedActor { private final String myName; private final String greeting; Greeter(String name, String greeting) { myName = name; this.greeting = greeting; } public static Props props(String name, String greeting) { return Props.create(Greeter.class, name, greeting); } public void onReceive(Object message) throws Exception { if (message instanceof Greet) { ((Greet)message).target.tell(AskName, self()); } else if (message == AskName) { sender().tell(new TellName(myName), self()); } else if (message instanceof TellName) { System.out.println(greeting + ", " + ((TellName)message).name); } } } }

В листинге 7 показана версия для Java 8 с лямбда-выражениями. Эта версия также несколько компактнее при реализации обработки сообщений, однако во всем остальном она ничем не отличается от предыдущей.

Листинг 7. Версия для Java 8
 import akka.japi.pf.ReceiveBuilder; ... private static class Greeter extends AbstractActor { private final String myName; private final String greeting; Greeter(String name, String greeting) { myName = name; this.greeting = greeting; receive(ReceiveBuilder. match(Greet.class, g -> { g.target.tell(AskName, self()); }). matchEquals(AskName, a -> { sender().tell(new TellName(myName), self()); }). match(TellName.class, t -> { System.out.println(greeting + ", " + t.name); }). build()); } public static Props props(String name, String greeting) { return Props.create(Greeter.class, name, greeting); } }

Передача свойств

Akka передает конфигурационные свойства в актор с помощью объектов Props. Каждый экземпляр Props обертывает копию аргументов конструктора, необходимых классу актора, вместе со ссылкой на этот класс. Эту информацию можно передать конструктору Props двумя способами. В примере в листинге 5 конструктор для актора передается конструктору Props как параметр с передачей по имени. Обратите внимание, что в этом способе не осуществляется немедленный вызов конструктора и передача результата; а производится передача вызова конструктора (разработчикам на Java это может показаться странным).

Другой способ передачи конфигурации актора конструктору Props состоит в передаче класса актора в качестве первого параметра, а аргументов конструктора для актора в качестве остальных параметров. Для примера в листинге 5 эта форма вызова будет иметь следующий вид: Props(classOf[Greeter], name, greeting).

Вне зависимости от используемой разработчиком формы конструктора Props значения, передаваемые новорожденному актору, должны быть сериализованы, чтобы в случае необходимости объекты Props можно было бы пересылать по сети в любое место, где будет исполняться экземпляр актора. В случае вызова конструктора посредством передачи параметра по имени, как это делается в листинге 5, замыкание этого вызова сериализуется, когда его необходимо отослать из JVM.

Рекомендуемая в Akka практика создания объектов Props в коде Scala состоит в определении генерирующего метода в сопутствующем объекте, как это сделано в листинге 5. Эта практика предотвращает любые возможные проблемы со случайным закрытием ссылки this на объект актора, когда разработчик использует для объектов Props подход с вызовом конструктора посредством передачи параметра по имени. Кроме того, сопутствующий объект – это отличное место для определения сообщений, которые получит актор, поскольку при этом вся связанная информация находится в одном месте. Для Java-акторов хорошо работает статический метод конструктора внутри класса актора (листинг 6).

Акторы, отсылающие сообщения

В конфигурации каждого актора Greeter в листинге 5 заданы имя и приветствие, однако когда актору дается указание приветствовать другого актора, ему сначала нужно узнать имя этого другого актора. Акторы Greeter исполняют эту задачу посредством отсылки этому другому актору специального сообщения AskName. Сообщение AskName само по себе не несет никакой информации, однако получивший это сообщение экземпляр Greeter знает, что ему нужно в ответ послать сообщение TellName, которое содержит имя отправителя TellName. Когда первый экземпляр Greeter получает в ответ сообщение TellName, он выводит на печать свое приветствие.

Каждое сообщение, отправленное актору, поступает с определенной дополнительной информацией, предоставленной Akka. Наиболее примечательной частью этой информации является ссылка ActorRef на отправителя этого сообщения. Разработчик может получить доступ к этой информации об отправителе в любой момент в процессе обработки сообщения, вызвав метод sender(), определенный в базовом классе актора. При обработке сообщения AskName акторы Greeter используют ссылку на отправителя, чтобы отправить ответ TellName надлежащему актору.

Akka позволяет отправлять сообщение от имени другого актора (мягкая форма кражи идентификационных данных), в результате чего актор, получающий сообщение, увидит в качестве отправителя другого актора. Эта функция часто оказывается полезной в системах акторов, особенно для обмена сообщениями типа запрос-ответ, в которых разработчик хочет, чтобы ответ поступал в какое-то другое место, а не в актор, сделавший запрос. Сообщения, отправляемые кодом приложения за пределы актора, будут по умолчанию в качестве отправителя использовать специальный Akka-актор с именем deadletter. Кроме того, актор deadletter всегда используется в том случае, когда сообщение невозможно доставить в актор. Это дает удобный способ отслеживания недоставленных сообщений в системе акторов, включая соответствующее журналирование (эта тема будет рассмотрена в следующей статье).

Типирование акторов

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

У программирования нетипированной системы акторов имеются практические преимущества. Разработчик может определять типы акторов — например, посредством определения набора сообщений, которые они способны обрабатывать — однако такой подход будет вводить в заблуждение. В Akka акторы могут изменять свое поведение (подробнее эта тема будет описана в следующей статье); таким образом, разные наборы сообщений могут подходить разным состояниям акторов. Кроме того, типы склонны препятствовать элегантной простоте модели акторов, которая рассматривает всех акторов как способных, по крайней мере, потенциально, обработать любое сообщение.

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

Сообщения и мутабельность

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

Таким образом, сообщения должны быть иммутабельными, причем не только на поверхностном уровне. Если какие-либо объекты являются частью данных сообщения, эти объекты также должны быть иммутабельными и так далее – вплоть до замыкания всего того, на что ссылается это сообщение. В настоящее время Akka не может принудительно реализовать это требование, однако создатели Akka хотят внедрить соответствующие ограничения в будущем. Если разработчик хочет, чтобы его код можно было применять с будущими версиями Akka, ему необходимо учитывать это требование уже сегодня.

Сравнение операций ask и tell

Код в листинге 5 для отправки сообщений использует стандартную операцию tell. Кроме этого, Akka позволяет использовать шаблон сообщения ask в качестве вспомогательной операции. Операция ask (показанная посредством оператора ? или функции ask) в качестве ответа отсылает сообщение с Future. В листинге 8 показан код листинга 5, реструктурированный для использования операции ask вместо операции tell.

Листинг 8. Использование операции ask
 import scala.concurrent.duration._ import akka.actor._ import akka.util._ import akka.pattern.ask object Hello4 extends App { import Greeter._ val system = ActorSystem("actor-demo-scala") val bob = system.actorOf(props("Bob", "Howya doing")) val alice = system.actorOf(props("Alice", "Happy to meet you")) bob ! Greet(alice) alice ! Greet(bob) Thread sleep 1000 system shutdown object Greeter { case class Greet(peer: ActorRef) case object AskName def props(name: String, greeting: String) = Props(new Greeter(name, greeting)) } class Greeter(myName: String, greeting: String) extends Actor { import Greeter._ import system.dispatcher implicit val timeout = Timeout(5 seconds) def receive = { case Greet(peer) => { val futureName = peer ? AskName futureName.foreach { name => println(s"$greeting, $name") } } case AskName => sender ! myName } } }

В показанном выше коде (листинг 8) сообщение TellName было заменено операцией ask. Объект future, возвращаемый операцией ask, имеет тип Future[Any], поскольку компилятор ничего не знает о возвращаемом результате. Когда future завершается, foreach обращается к неявному диспетчеру, определенному утверждением import system.dispatcher, для исполнения println. Если future не завершается с ответным сообщением в рамках допустимого временного интервала (еще одно неявное значение; в данном случае оно составляет 5 секунд), то вместо этого future завершается с исключением по тайм-ауту.

На заднем плане шаблон ask создает специализированный одноразовый актор, который выступает в качестве посредника при обмене сообщениями. Этот посредник получает переданный Promise и сообщение, подлежащее отсылке, вместе со ссылкой на целевой актор. Посредник отсылает сообщение, а затем ждет ожидаемого ответного сообщения. После получения ответа он исполняет promise и завершает future, используемые исходным актором.

Использование подхода на основе ask имеет некоторые ограничения. В частности, чтобы избежать представления состояния актора (и возможных проблем поточного исполнения), разработчик должен следить за тем, чтобы не использовать никаких мутабельных состояний из актора в коде, исполняемом при завершении future. На практике для сообщений, пересылаемых между акторами, обычно легче использовать шаблон tell. Шаблон ask полезен в том случае, когда коду приложения (например, основной программе, которая запускает систему акторов и создает первоначальных акторов) необходимо получать ответ от актора (типированного или нетипированного).

Акторы на эпизодические роли

Без колебаний вводите нового актора в свой проект всякий раз, когда это помогает чисто обрабатывать асинхронные операции

Однократный актор, созданный шаблоном ask, - это пример правильного подхода к проектированию при использовании Akka. Во многих случаях желательно структурировать систему акторов таким образом, чтобы промежуточные шаги обработки выполнялись специальными акторами, созданными для этой конкретной цели. Типичный пример – необходимость объединения различных асинхронных результатов перед переходом к следующему этапу обработки. Если разработчик использует сообщения для разных результатов, он может поручить актору собирать результаты до момента их полной готовности, а затем перейти к следующему этапу. В своей основе это обобщение одноразового актора, используемого шаблоном ask.

Акторы Akka потребляют мало ресурсов (примерно по 300 - 400 байт на экземпляр актора, плюс ресурсы хранения, используемые классом актора), таким образом, разработчик может безопасно структурировать свой проект таким образом, чтобы использовать большое количество акторов, когда это целесообразно. Использование специализированных акторов помогает разработчику сохранить простоту и легкость понимания своего кода, что при написании параллельных программ является еще более важным преимуществом, чем в случае последовательных программ. Без колебаний вводите нового актора в свой проект всякий раз, когда это помогает чисто обрабатывать асинхронные операции.

Заключение

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

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


Ресурсы для скачивания


Похожие темы

  • Оригинал статьи: JVM concurrency: Acting asynchronously with Akka
  • Видео: IBM Bluemix in action. В этой демонстрации Дейвид Барнс (David Barnes) показывает, как проектировать, создавать и развертывать приложения в облаке..
  • Scalable Scala: Денис Сосноски, автор данного цикла, делится опытом и внутренней информацией о его содержании и о разработке на языке Scala в целом.
  • Демонстрационный программный код для данной стать. Загрузите полный демонстрационный код для данной статьи из репозитария ее автора на сайте GitHub.
  • Akka.io: всеобъемлющий источник ресурсов по Akka, включая полный набор документации для приложений на Scala и на Java.
  • Scala: современный функциональный язык, работающий на платформе JVM.
  • A Java actor library for parallel execution (Барри Фигенбаум (Barry Feigenbaum), developerWorks, май 2012 г.). Введение в библиотеку μJavaActors — это пакет основанных на Java акторов, потребляющих мало ресурсов и обеспечивающих высокую степень параллелизма при исполнении традиционных Java-приложений. Кроме того, можно посмотреть дополнительную видеопрезентацию Modernize common concurrency patterns with actors.

Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java, Open source
ArticleID=1033286
ArticleTitle=Одновременное исполнение на платформе JVM: Асинхронное исполнение с помощью Akka
publish-date=06082016