Вторая волна разработки Java-приложений: Представляем Kilim

Инфраструктура на основе акторов для поддержки параллелизма в Java

Параллельное программирование занимает одно из центральных мест во второй волне Java™-разработки, однако, скорее всего, речь идет не о многопоточности. В этой статье Эндрю Гловер рассказывает о преимуществах акторов перед потоками для параллельной обработки данных на многоядерных системах. Вы также узнаете о Kilim - акторной инфраструктуре на основе обмена сообщениями, которая связывает воедино подходы к созданию параллельных и распределенных приложений.

Эндрю Гловер, президент компании, Stelligent Incorporated

Эндрю ГловерЭндрю Гловер является президентом компании Stelligent Incorporated , которая помогает другим фирмам решать проблемы качества программного обеспечения. Для этого используются эффективные стратегии тестирования и технологии непрерывной интеграции, которые позволяют коллективам разработчиков постоянно контролировать качество кода, начиная с ранних стадий разработки. Просмотрите блог Энди , там можно найти список его публикаций.



01.04.2011

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

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

Kilim предоставляет интуитивно понятную модель акторов, которая, как будет продемонстрировано ниже, превращает параллельное программирование в легкую прогулку.

Эра многоядерных архитектур

В 2005 г. Герб Саттер (Herb Sutter) опубликовал вызвавшую широкий резонанс статью под заголовком "The Free Lunch is Over: A Fundamental Turn Toward Concurrency in Software" ("Бесплатный сыр кончился. Фундаментальный переход к параллельному программированию"). Она опровергла распространенное убеждение в наращивании тактовой частоты процессоров в силу закона Мура.

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

Время доказало его правоту. Производители процессоров достигли определенного предела тактовой частоты процессоров, которая стабилизировалась на уровне около 3,5 ГГц. При этом закон Мура никуда не делся, поскольку производительность чипов продолжает повышаться за счет увеличения количества ядер.

Об этой серии

Характерные черты разработки на Java существенно изменились с момента выхода первого релиза платформы. Благодаря зрелости открытых инфраструктур и аренде надежных сред развертывания, есть возможность собирать, тестировать и поддерживать Java-приложения без серьезных временных и денежных затрат. В этой серии Эндрю Гловер описывает ряд технологий, лежащих в основе новой парадигмы создания приложений на Java.

Саттер отметил, что параллельное программирование позволит пользоваться преимуществами многоядерных архитектур. При этом он добавил, что "разработчикам как воздух необходима модель параллельного программирования более высокого уровня, чем та, которую предлагают современные языки".

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


Акторная модель

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

Акторы выступают в роли изолированных и независимых друг от друга объектов, не использующих разделяемую память при взаимодействии. Эта модель не подразумевает синхронизирующие блокировки, что устраняет связанные с ними потенциальные проблемы, такие как взаимные блокировки или печально известные потерянные изменения (lost-update problem). При этом акторы были специально созданы для параллельного, а не последовательного выполнения. Таким образом, эта модель, самостоятельно координирующая параллельное выполнение, значительно безопаснее (в силу отсутствия ошибок синхронизации). Другими словами, она упрощает параллельное программирование.

Акторы появились далеко не вчера. В некоторых языках, например в Erlang и Scala, именно на них, а не на потоках, базируется модель параллелизма. Успех Erlang в корпоративных системах (этот язык был разработан в компании Ericsson и успешно применяется в телекоммуникациях) привлек внимание к акторной модели, после чего она стала рассматриваться в качестве реальной альтернативы в других языках. Erlang – это великолепный пример безопасного подхода к параллельному программированию на основе акторов.

К сожалению, акторная модель напрямую не поддерживается платформой Java, однако ее можно использовать в том или ином виде. Благодаря поддержке множества языков для JVM вы можете работать с акторами при помощи Scala или Groovy (в разделе Ресурсы приведена ссылка на GPars - библиотеку акторов для Groovy). Кроме того, существует ряд библиотек, реализующих эту модель в Java, в частности Kilim.


Акторы в Kilim

Kilim - это написанная на Java библиотека, реализующая модель акторов, которые представлены типом данных Task. Они являются легковесными потоками и взаимодействуют между собой при помощи типа Mailbox (почтовый ящик).

Почтовые ящики могут принимать сообщения любого типа, например java.lang.Object. Экземпляры Task, в свою очередь, могут посылать сообщения в виде строк или объектов нужного типа данных - это остается на усмотрение разработчика.

В Kilim все связывается воедино при помощи сигнатур методов. Например, если вы хотите выполнять какой-то метод параллельно, то достаточно добавить throws Pausable к его сигнатуре. Таким образом, создание классов, выполняющих параллельную обработку данных, не сложнее реализации интерфейса Runnable или расширения класса Thread в Java. При этом вам не придется использовать некоторые конструкции, ассоциирующиеся с Runnable или Thread, например ключевое слово synchronized.

Наконец, волшебные возможности Kilim достигаются во многом за счет специального процесса обработки байт-кода классов, называемого weaver (ткач). Методы, выбрасывающие исключения типа Pausable, обрабатываются планировщиком на этапе выполнения, который также входит в состав Kilim. Он управляет пулом потоков ядра ограниченного размера, позволяющим выполнять множество легковесных потоков с минимальными затратами на запуск и переключение контекста. Управление локальным стеком каждого потока также выполняется автоматически.

Основное преимущество Kilim заключается в упрощении создания параллельных потоков: вам достаточно унаследовать свой класс от Task и реализовать метод execute. После компиляции класса запустите над ним процесс weaver, и все готово!

На первый взгляд, Kilim выглядит несколько нестандартно, однако у него есть серьезные преимущества. Модель акторов, реализованная в Kilim, упрощает и делает более безопасным создание объектов, которые взаимодействуют асинхронным образом с другими подобными объектами. Разумеется, вы можете добиться аналогичного поведения при помощи стандартной модели потоков в Java, расширяя класс Thread, однако это потребует от вас дополнительных усилий по управлению блокировками и синхронизацией. Таким образом, общий вывод прост: акторы делают программирование многопоточных приложений проще и безопаснее.


Kilim в действии

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

В качестве примера работы с почтовыми ящиками в Kilim рассмотрим два актора (классы Calculator и DeferredDivision, расширяющие Task). Они будут выполняться параллельно, взаимодействуя друг с другом. Объект типа DeferredDivision будет создавать делимое и делитель, не пытаясь вычислить результат деления. Представьте себе, что операция может занимать длительное время, поэтому DeferredDivision делегирует ее выполнение классу Calculator.

Акторы обмениваются данными при помощи общего объекта Mailbox, принимающего объекты типа Calculation. Тип данных сообщений довольно прост: он включает делимое, делитель, а также результат деления, который устанавливается классом Calculator. После вычисления результата Calculator помещает сообщение обратно в почтовый ящик.

Класс Calculation

Класс Calculation приведен в листинге 1. Обратите внимание, что он не зависит от классов Kilim, а представляет собой стандартный класс Java-объектов (Java Bean).

Листинг 1. Класс сообщений Calculation
import java.math.BigDecimal;

public class Calculation {
 private BigDecimal dividend;
 private BigDecimal divisor;
 private BigDecimal answer;

 public Calculation(BigDecimal dividend, BigDecimal divisor) {
  super();
  this.dividend = dividend;
  this.divisor = divisor;
 }

 public BigDecimal getDividend() {
  return dividend;
 }

 public BigDecimal getDivisor() {
  return divisor;
 }

 public void setAnswer(BigDecimal ans){
  this.answer = ans;
 }

 public BigDecimal getAnswer(){
  return answer;
 }

 public String printAnswer() {
  return "The answer of " + dividend + " divided by " + divisor +
    " is " + answer;	
 }
}

Класс DeferredDivision

Впервые вы увидите обращение к классам Kilim в классе DeferredDivision. Он выполняет ряд действий, однако в целом его задача достаточно проста: создавать экземпляры Calculation со случайными числами типа BigDecimal и передавать их актору Calculator. Кроме того, этот класс проверяет содержимое общего MailBox на наличие сообщений (экземпляров Calculation). Если полученный экземпляр Calculation содержит результат деления, он выводится на экран.

Листинг 2. DeferredDivision генерирует делимые и делители случайным образом
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.Date;
import java.util.Random;

import kilim.Mailbox;
import kilim.Pausable;
import kilim.Task;

public class DeferredDivision extends Task {

 private Mailbox<Calculation> mailbox;

 public DeferredDivision(Mailbox<Calculation> mailbox) {
  super();
  this.mailbox = mailbox;
 }

 @Override
 public void execute() throws Pausable, Exception {
  Random numberGenerator = new Random(new Date().getTime());
  MathContext context = new MathContext(8);
  while (true) {
   System.out.println("I need to know the answer of something");
   mailbox.putnb(new Calculation(
     new BigDecimal(numberGenerator.nextDouble(), context), 
     new BigDecimal(numberGenerator.nextDouble(), context)));
   Task.sleep(1000);
   Calculation answer = mailbox.getnb(); // no block
   if (answer != null && answer.getAnswer() != null) {
    System.out.println("Answer is: " + answer.printAnswer());
   }
  }
 }
}

Как видно из листинга 2, класс DeferredDivision является наследником класса Task в Kilim, являющегося базовым в модели акторов. Обратите внимание, что класс перегружает метод execute класса Task. Сигнатура этого метода декларирует выброс исключения Pausable, поэтому выполнение будет регулироваться планировщиком Kilim. Другими словами, Kilim будет отвечать за безопасное параллельное выполнение данного метода.

Метод execute создает экземпляры Calculation и помещает их в Mailbox. Это делается неблокирующим образом при помощи метода putnb.

Заполнив почтовый ящик, объект DeferredDivision засыпает. Однако засыпает именно легковесный поток, управляемый Kilim, а не поток операционной системы. Проснувшись, актор проверяет ящик на наличие объектов Calculation. Эта проверка также выполняется в неблокирующем режиме, что означает, что getnb может вернуть null. Если DeferredDivision обнаруживает экземпляр Calculation, чей метод getAnswer возвращает непустое значение (т.е. не то сообщение, которое ждет вычисления операции классом Calculator), то оно выводится в консоль.

Класс Calculator

С другой стороны к объекту Mailbox обращается Calculator. Подобно актору DeferredDivision, показанному в листинге 2, Calculator также расширяет Task и реализует метод execute. Важно отметить, что оба актора используют общий почтовый ящик, в противном случае они не смогли бы взаимодействовать. Для этого конструкторы обоих классов принимают экземпляр Mailbox в качестве параметра (листинг 3).

Листинг 3. Основная работа выполняется классом Calculator
import java.math.RoundingMode;

import kilim.Mailbox;
import kilim.Pausable;
import kilim.Task;

public class Calculator extends Task{

 private Mailbox<Calculation> mailbox;

 public Calculator(Mailbox<Calculation> mailbox) {
  super();
  this.mailbox = mailbox;
 }

 @Override
 public void execute() throws Pausable, Exception {
  while (true) {			
   Calculation calc = mailbox.get(); // blocks
   if (calc.getAnswer() == null) {
    calc.setAnswer(calc.getDividend().divide(calc.getDivisor(), 8, 
      RoundingMode.HALF_UP));				
    System.out.println("Calculator determined answer");
    mailbox.putnb(calc);
   }
   Task.sleep(1000);
  }
 }
}

Как и в DeferredDivision, метод execute класса Calculator находится в бесконечном цикле, проверяя содержимое общего экземпляра Mailbox. Разница в том, что Calculator вызывает метод get, который блокирует его выполнение. Таким образом, класс выполняет операцию деления только после появления сообщения в почтовом ящике. После этого Calculator помещает измененный экземпляр Calculation обратно в ящик (при этом выполнение не блокируется) и засыпает на определенный промежуток времени. Вызовы sleep в обоих акторах нужны исключительно для того, чтобы было легче читать консольный вывод.


Процесс weaver в Kilim

Ранее упоминалось о том, что Kilim модифицирует байт-код классов при помощи процесса под названием weaver, который выполняется после компиляции. Он добавляет специальный код в классы и методы, сигнатура которых включает Pausable.

Запуск weaver не представляет сложностей. Например, в листинге 4 показано его выполнение при помощи Ant. Все, что от вас требуется, – это указать классу Weaver, где находятся акторы и куда следует поместить результирующий байт-код. В данном случае Weaver обрабатывает классы в директории target/classes и в ней же сохраняет результаты.

Листинг 4. Вызов процесса weaver при помощи Ant
        <target name="weave" depends=
        "compile" description="handles Kilim byte code weaving">
 <java classname="kilim.tools.Weaver" fork="yes">
  <classpath refid="classpath" />
  <arg value="-d" />
  <arg value="./target/classes" />
  <arg line="./target/classes" />
 </java>
</target>

После этого вы можете запустить ваше приложение, не забыв добавить JAR-файлы Kilim в его classpath.


Использование Kilim на этапе выполнения приложения

Запуск двух акторов не сильно отличается от запуска обыкновенных потоков в Java. Оба актора следует создать, передав в их конструкторы общий объект sharedMailbox, а затем вызвать методы start, тем самым запустив их выполнение (листинг 5).

Листинг 5. Класс для запуска акторов
import kilim.Mailbox;
import kilim.Task;

public class CalculationCooperation {
 public static void main(String[] args) {
  Mailbox<Calculation> sharedMailbox = new Mailbox<Calculation>();

  Task deferred = new DeferredDivision(sharedMailbox);
  Task calculator = new Calculator(sharedMailbox);

  deffered.start();
  calculator.start();

 }
}

В листинге 6 показан фрагмент консольного вывода, сгенерированного акторами. Если вы запустите этот пример, то результат, скорее всего, будет выглядеть несколько по-другому, однако логика выполнения действий останется той же. В листинге 6 показаны запросы, генерируемые актором DeferredDivision, и результаты деления, вычисляемые актором Calculator.

Листинг 6. Ваши результаты будут отличаться от приведенных ниже, поскольку акторы выполняются недетерминированно
[java] I need to know the answer of something
[java] Calculator determined answer
[java] Answer is: The answer of 0.36477377 divided by 0.96829189 is 0.37671881
[java] I need to know the answer of something
[java] Calculator determined answer
[java] Answer is: The answer of 0.40326269 divided by 0.38055487 is 1.05967029
[java] I need to know the answer of something
[java] Calculator determined answer
[java] Answer is: The answer of 0.16258913 divided by 0.91854403 is 0.17700744
[java] I need to know the answer of something
[java] Calculator determined answer
[java] Answer is: The answer of 0.77380722 divided by 0.49075363 is 1.57677330

Заключение

Модель акторов упрощает параллельное программирование, предлагая безопасный механизм обмена сообщениями между процессами (акторами). В настоящее время она реализована в различных языках и библиотеках. Я советую обратить внимание на акторы в Erlang, а также на их реализацию в Scala. Обе реализации достаточно аккуратные в том, что касается синтаксиса.

Если вы хотите использовать акторы в приложениях на Java, то придется выбирать между Kilim и аналогичными решениями (см. раздел Ресурсы). Акторы, разумеется, не решат все ваши проблемы, но позволят легче создавать параллельные приложения, извлекая преимущества из многоядерных архитектур.

Ресурсы

Научиться

Получить продукты и технологии

  • GPars - Groovy Parallel Systems: ознакомьтесь с реализацией инфраструктуры акторов в Groovy, предоставляющей возможности по созданию многопоточных приложений для платформы Java. (EN)
  • Загрузите Kilim - инфраструктуру обмена сообщениями в Java, которая предоставляет ультралегковесные потоки для быстрого и безопасного взаимодействия потоков, не требующего копирования данных. (EN)

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=644226
ArticleTitle=Вторая волна разработки Java-приложений: Представляем Kilim
publish-date=04012011