Typesafe Stack — современная программная платформа, которая обеспечивает возможность создания легко масштабируемых программных систем на Java и Scala. Стек функционирует на JVM, включает в себя фрэймворки Akka 2.0 и Playframework 2.0. Ядром платформы, которое обеспечивает практически неограниченную масштабируемость разрабатываемой системы, является библиотека Akka, реализующая многозадачность на основе модели акторов.
Модель акторов
Строгое определение модели акторов, а также историю ее возникновения и фундаментальные концепции можно найти в википедии. Если определить коротко, модель акторов исходит из философии, что все вокруг является акторами. Это похоже на философию объектно-ориентированного программирования с тем лишь отличием, что в модели акторов вычисления по своей сути совпадают во времени, когда как в объектно-ориентированном программировании, как правило, программы выполняются последовательно. В модели акторов отсутствует разделяемое состояние, а обмен данными осуществляется с помощью механизма передачи сообщений от одного актора к другому. Каждый актор имеет очередь входящих сообщений, которые обрабатываются им последовательно. За счет отсутствующего разделяемого состояния и последовательной обработки сообщений распараллеливание вычислений в программах, в основе которых лежит модель акторов, сильно упрощается, а надежность таких программ возрастает.
Akka
Как написано выше, Akka реализует модель акторов на JVM. Акторы иначе называют легковесными потоками (green threads). Такие потоки действительно легковесные, их количество может достигать ~2.7 млн на 1 ГБ оперативной памяти, а количество передаваемых сообщений более 20 млн/сек на одном узле. Есть возможность организации взаимодействия акторов через сеть, что обеспечивает практически неограниченную масштабируемость разрабатываемого приложения. Более подробную информацию о возможностях Akka можно найти на официальном сайте.
Пример использования Typesafe и Akka
Оригинал: здесь
Начать использование Akka в составе Typesafe Stack довольно просто. Процесс настройки сводится к скачиванию дистрибутива стека и его установке. Хотя для разработки можно использовать как Java так и Scala, использование последнего делает программу особенно стройной и легкой для чтения и понимания, поэтому далее в примере будем использовать Scala.
Постановка задачи
В данном примере мы применим акторы для вычисления значения числа ПИ. Данная операция интенсивно использует процессор, поэтому Akka позволит реализовать параллельное вычисление, масштабируемое на многоядерной машине. Используемый алгоритм хорошо распараллеливается т.к. каждое из вычислений является независимым, что прекрасно проецируется на модель акторов. Формула для вычисления имеет следующий вид:
Основной актор разделит данный ряд на составляющие и отправит каждый из членов на вычисление вспомогательному актору (по одному вспомогательному актору на каждый член ряда). По завершению вычисления каждый из вспомогательных акторов вернет результат основному актору. После получения результатов всех вычислений, основной актор сформирует ответ.
Создание проекта
Во-первых необходимо создать проект из шаблона giter8 (Typesafe Stack должен быть установлен):
c:temp>g8 typesafehub/akka-scala-sbt
Akka 2.0 Project Using Scala and sbt
organization [org.example]: akka.tutorial
package [org.example]: akka.tutorial.first.scala
name [Akka Project In Scala]: PI Calculator
akka_version [2.0]:
version [0.1-SNAPSHOT]:
Applied typesafehub/akka-scala-sbt.g8 in pi-calculator
Теперь, когда проект создан, переходим в папку проекта:
c:temp>cd pi-calculator
Начинаем писать код
Прежде всего необходимо создать в соответствующей папке (./src/main/scala/akka/tutorial/first/scala/
) файл Pi.scala
и импортировать необходимые библиотеки:
import akka.actor._
import akka.routing.RoundRobinRouter
import akka.util.Duration
import akka.util.duration._
Создание шаблонов сообщений
Уточним архитектуру нашего приложения вкратце описанную выше. Имеется один Master
актор, который создает множество Worker
акторов и инициализирует вычисление. Для этого он делит всю задачу на мелкие операции и посылает эти операции для выполнения Worker
акторам. После выполнения операций, Worker
акторы возвращают результаты для агрегации. После завершения вычисления Master
актор отправляет результат Listener
актору, который выводит его на экран.
Опираясь на данное описание создадим сообщения, которые будут передаваться в системе:
Calculate
— отправляетсяMaster
актору для старта вычисленияWork
— отправляетсяMaster
акторомWorker
акторам, содержит описания операции для выполненияResult
— отправляетсяWorker
акторамиMaster
акторуPiApproximation
— отправляетсяMaster
акторомListener
актору, содержит результат вычисления и время, затраченное на вычисление
Сообщения, отправляемые акторам должны быть неизменяемыми для исключения общего изменяемого состояния. В Scala для этого отлично подходят case classes. Также мы создадим общий базовый trait для сообщений, который обозначим как sealed для исключения бесконтрольного добавления сообщений:
sealed trait PiMessage case object Calculate extends PiMessage case class Work(start: Int, nrOfElements: Int) extends PiMessage case class Result(value: Double) extends PiMessage case class PiApproximation(pi: Double, duration: Duration)
Создание Worker
актора
Сейчас мы можем создать Worker
актор. Это делается путем подмешивания трэйта Actor
и определения метода receive
. Данный метод является обработчиком входящих сообщений:
class Worker extends Actor { // calculatePiFor ... def receive = { case Work(start, nrOfElements) ⇒ sender ! Result(calculatePiFor(start, nrOfElements)) // perform the work } }
Как вы можете видеть, мы добавили обработчик сообщения Work
, который создает ответное сообщение Result
после выполнения операции calculatePiFor
и посылает его обратно отправителю. Теперь реализуем calculatePiFor
:
def calculatePiFor(start: Int, nrOfElements: Int): Double = { var acc = 0.0 for (i ← start until (start + nrOfElements)) acc += 4.0 * (1 - (i % 2) * 2) / (2 * i + 1) acc }
Создание Master
актора
Master
актор немного сложнее, т.к. в его конструкторе создается циклический роутер для облегчения распределения операций между Worker
акторами:
val workerRouter = context.actorOf( Props[Worker].withRouter(RoundRobinRouter(nrOfWorkers)), name = "workerRouter")
Теперь напишем сам Master
актор. Данный актор создается со следующими параметрами:
- nrOfWorkers — количество
Worker
акторов - nrOfMessages — общее количество цепочек операций
- nrOfElements — количество операций в одной цепочке, вычисляемых каждым
Worker
актором
Итак имеем следующий код:
class Master(nrOfWorkers: Int, nrOfMessages: Int, nrOfElements: Int, listener: ActorRef) extends Actor { var pi: Double = _ var nrOfResults: Int = _ val start: Long = System.currentTimeMillis val workerRouter = context.actorOf( Props[Worker].withRouter(RoundRobinRouter(nrOfWorkers)), name = "workerRouter") def receive = { // handle messages ... } }
Кроме описанных выше параметров, в Master
актор передается объект ActorRef
, представляющий собой ссылку на Listener
актор. Следует отметить, что передача сообщений между акторами всегда производится посредством таких ссылок.
Но это еще не все, т.к. мы не реализовали обработчики сообщений, которые и напишем сейчас:
def receive = { case Calculate ⇒ for (i ← 0 until nrOfMessages) workerRouter ! Work(i * nrOfElements, nrOfElements) case Result(value) ⇒ pi += value nrOfResults += 1 if (nrOfResults == nrOfMessages) { // Send the result to the listener listener ! PiApproximation(pi, duration = (System.currentTimeMillis - start).millis) // Stops this actor and all its supervised children context.stop(self) } }
В целом по коду должно быть все понятно, следует только отметить, что после завершения вычисления и отправки результата на печать, Master
актор останавливается командой context.stop(self)
.
Создание Listener
актора
Реализация данного актора вполне простая. Он принимает сообщение PiApproximation
от Master
актора, печатает результат и останавливает систему акторов:
class Listener extends Actor { def receive = { case PiApproximation(pi, duration) ⇒ println("ntPi approximation: tt%sntCalculation time: t%s" .format(pi, duration)) context.system.shutdown() } }
Написание объекта-приложения
Теперь осталось только написать объект-приложение и наша программа готова:
object Pi extends App { calculate(nrOfWorkers = 4, nrOfElements = 10000, nrOfMessages = 10000) // actors and messages ... def calculate(nrOfWorkers: Int, nrOfElements: Int, nrOfMessages: Int) { // Create an Akka system val system = ActorSystem("PiSystem") // create the result listener, which will print the result and // shutdown the system val listener = system.actorOf(Props[Listener], name = "listener") // create the master val master = system.actorOf(Props(new Master( nrOfWorkers, nrOfMessages, nrOfElements, listener)), name = "master") // start the calculation master ! Calculate } }
Запуск приложения
Проще всего запустить приложение из sbt. Для этого необходимо перейти в папку с приложением и набрать команду sbt
. Затем выполняем следующую последовательность команд:
c:temppi-calculator>sbt
[info] Loading project definition from C:temppi-calculatorproject
[info] Set current project to PI Calculator (in build file:/C:/temp/pi-calculator/)
> compile
[success] Total time: 0 s, completed 20.03.2012 16:33:03
> run
[info] Running akka.tutorial.first.scala.Pi
Pi approximation: 3.1415926435897883
Calculation time: 423 milliseconds
[success] Total time: 1 s, completed 20.03.2012 16:33:25
Если уменьшить количество акторов до 1, то результат изменится:
> run
[info] Running akka.tutorial.first.scala.Pi
Pi approximation: 3.1415926435897883
Calculation time: 1160 milliseconds
[success] Total time: 2 s, completed 20.03.2012 16:35:16
Если увеличить до 8, то результат будет таким:
> run
[info] Running akka.tutorial.first.scala.Pi
Pi approximation: 3.1415926435897905
Calculation time: 388 milliseconds
[success] Total time: 1 s, completed 20.03.2012 16:36:55
Результат предсказуемый, учитывая то, что испытания проводились на 4-х ядерной машине. Так увеличение количества акторов до 16 не дает практически никакого прироста производительности:
> run
[info] Running akka.tutorial.first.scala.Pi
Pi approximation: 3.141592643589789
Calculation time: 372 milliseconds
[success] Total time: 1 s, completed 20.03.2012 16:40:04
Надеюсь было увлекательно. Следующий пост будет про Play 2.0. Удачи!
Автор: tolyasik