Download - Курсы актерского мастерства
Школа актерского мастерства
@remeniuk
• 70-ые, концепция «Актеров»
• 80-ые, Erlang, популяризация
и развитие
– OTP – фреймворк для создания
надежных систем,
толерантных к ошибкам
• 00-ые, Scala, cовременное
представление
Почему актеры?
• Параллелизм без страха
• Хороший дизайн
• Простая расширяемость
– location transparency
• Инструментарий для создания надежных
систем
WIN!
• scala.actors
• Akka
• Scalaz
• Kilim
• Lift
Будущее: scala.actors + Akka
Как будет происходить объединение scala.actors и
Akka?
Предположение: единый API, и fault-tolerance
стек из Akka.
scala.actors
actor{ loop{ react{ case msg => } } }
actor{ while(true){ receive{ case msg => } } }
! !! !? ?
Thread-based Event-driven
Актер с состоянием
• Модель publisher/subscriber – тривиальный подход - хранение в переменной
protected var subscribers = List[Subscriber]()
loop {
react{
case AddSubscriber(subscriber) =>
subscribers = subscriber :: subscribers
case Publish(event) =>
subscribers.foreach(_ ! event)
Актер с состоянием
Хранение состояния в Erlang
loop(State) ->
receive
Msg ->
NewState = whatever(Msg),
loop(NewState)
end.
Передача состояния в рекурсии: • «чистый» функциональный подход • требует поддержки оптимизации рекурсии
Модель publisher/subscriber • Передача состояния в рекурсии
def loop(subscribers: List[Subscriber]): Unit
= react{
case AddSubscriber(subscriber) =>
loop(subscriber :: subscribers)
case Publish(event) =>
subscribers.foreach(_ ! event)
loop(subscribers)
}
def act = loop(Nil)
Идемпотентный актер
Идемпотентный актер
f(x) = f(f(x))
Актер, устойчивый к получению
дупликатов сообщений
Nested react/receive (вложенный
обработчик)
• обрабатывается только первое
сообщение – остальные пропускаются
loop {
react {
case Event(evt) =>
// необходимые вычисления...
def skip: Unit = reactWithin(0) {
case Event(event) if(event == evt) => skip
case TIMEOUT =>
}
skip
Nested react/receive способен на
большее: • обработка с приоритетом
reactWithin(0){
case Event(evt: String) =>
subscribers.foreach(_ ! evt)
case TIMEOUT => react {
case AddSubscriber(subscriber) =>
subscribers = subscriber :: subscribers
case Event(evt) =>
subscribers.foreach(_ ! evt)
Типизированный актер
Типизированный актер
• Процессы в Erlang не типизированы – Придает гибкость модели
• Актеры, по своей природе, динамичны
• HotSwap обработчика
• Базовые актеры в Scala и Akka,
следующие идеологии Erlang, тоже не
типизированы
Типизированные актеры иногда полезны:
• !?, !!, !!! – известный тип «ответа»
• специализация обработчиков
• Scala <2.7.x - InputChannel[T]
• Scala 2.8.x+ - Reactor[T]
sealed trait Message
class Foo extends Message
class Bar extends Message
class DatatypeReceiver[T >: Null <: Message]
extends Reactor[T]
Принимает сообщения
подтипа Message
Типизация актеров позволяет использовать
всю мощь системы типов Scala
– Typeclass паттерн
implicit val fooReceiver = new DatatypeReceiver[Foo]
implicit val barReceiver = new DatatypeReceiver[Bar]
def send[T >: Null <: Message : DatatypeReceiver]
(message: T) =
implicitly[DatatypeReceiver[T]] ! message
send(new Foo)
Находит в контексте обработчик
нужного типа
Альтернативный подход
• декларирование возможных типов
сообщений в объекте-компаньоне
• паттерн Active Object
– типизированные актеры в Akka:
trait Service {
def serve(arg: String)
}
class ServiceImpl extends TypedActor with Service {
def serve(arg: String) = //…
}
service.serve(“foo”)
Выполняется асинхронно
Scatter/Gather рассеять/собрать
Scatter/Gather рассеять/собрать
Тривиальное решение – блокирующий
синхронный аггрегатор
• аггрегатор не может обрабатывать новые
сообщения, пока собирает ответы
react{
case Event(event) =>
val results = Futures.awaitAll(1000,
subscribers.map(_ !! event):_*)
// обработка собранных ответов...
reply(results)
FAIL!
reply(res)
Можно прибегнуть к хитрости – перенести
ожидание, обработку и ответ в другой поток • неэффективное использование – выделенный
поток просто ждет
react{
case Event(event) =>
actor{
val results = Futures.awaitAll(1000,
subscribers.map(_ !! event):_*)
// обработка собранных ответов...
reply(results)
}
Aсинхронная аггрегация – решение в Erlang-
стиле
1) канал для передачи результатов аггрегации
2) асинхронная сборка результатов:
• ответы «рабочих» обрабатываются
аггрегатором наравне с другими
сообщениями
• промежуточные результаты передаются
рекурсивно
class AsynchAggregator(workers: Iterable[Subscriber],
replyChannel: Channel[Any]) extends Actor {
def loop(results: List[Any]): Unit = react {
case event: Event[Any] =>
workers foreach(_ ! event); loop(results)
case Result(res) =>
if(results.size != workers.size - 1) loop(res :: results)
else { // обработка собранных результатов...
replyChannel ! (res :: results); loop(Nil)
}
reply(Result(res))
val aggregator = new AsynchAggregator(subscribers,
replyChannel)
aggregator ! Event("hello")
replyChannel.receive{
case message => // результат работы аггрегатора
}
Клиентская сторона (синхронное окружение)
• клиент ждет сообщение в канале, блокируя
поток
val aggregator = new AsynchAggregator(subscribers,
replyChannel)
actor { aggregator ! Event("hello") }
replyChannel.receive{
case message => // результат работы аггрегатора
}
Клиентская сторона (синхронное окружение)
• клиент ждет сообщение в канале, блокируя
поток
Другие библиотеки
• Функциональный подход c Promises в
Scalaz • Псевдо-императивный подход с
Dataflow в Akka
Scalaz Promise[T]
Актер-работник асинхронно дает
аггрегатору обещание (promise), и выполняет
его
react {
case Message(msg) =>
val rez = new Promise[String]()(Strategy.Sequential)
reply(rez)
rez.fulfill{Thread sleep 1000; “Hello!”}
}
Scalaz Promise[T]
Аггрегатор дает клиенту обещание результата,
декларируя будущую аггрегацию и обработку
case msg: Message =>
val res = workerz.map(worker => (worker !? msg)
.asInstanceOf[Promise[String]])
.sequence
.map{results =>
// обработка
results
}
reply(res)
PROFIT!!!
List[Promise[String]] => Promise[List[String]]
Akka Dataflow
Dataflow concurrency – декларативная модель
безопасного параллелизма
Dataflow-переменная
• Изменение значения
вызывает цепную реакцию
• Инициализируется единожды
• Может безопасно
использоваться в
многопоточном окружении
def sample{
val x0, x1, y0, y1 = Promise[Int]()
flow{
y0 << x0() + 1; println("y0 = " + y0())
y1 << x1() + y0(); println("y1 = " + y1())
}
flow{ x0 << 1 }
flow{ x1 << 2 }
}
Akka Dataflow*
scala> sample y0 = 2 y1 = 4
*базируется на continuations → нет lock’ов
Dataflow-переменные
Присвоение значения
val result = Promise[String]()
val promises = List.fill(recipients.size)(Promise[String]())
recipients.zip(promises).map{case (recipient, promise) =>
(recipient !!! msg).map{result: String =>
flow{
promise << result }
}
}
Как применить dataflow для scatter /
gather?
• Рассылка, и асинхронная инициализация
dataflow-переменных
def gather(promises: List[CompletableFuture[String]],
result: String = ""): String @cps[Future[Any]] =
promises match {
case head :: tail =>
gather(tail, head() + result)
case Nil => result
}
flow {
result << gather(promises)
}
Аггрегатор
Балансировка нагрузки
• Балансировка на уровне инфраструктуры
• Актер-роутер, работающий с пулом актеров, выбирающий «работника» согласно некому
алгоритму балансировки
• Балансировка на уровне
планировщика/диспетчера потоков
• scala.actors: fork-join pool • Akka: work-stealing dispatchers
Актер-роутер
case class Ready(actor:
Actor)
def worker = actor{
loop{
react{
case msg: Ready =>
reply(this)
case msg =>
//обработка
class Balancer[A <: Actor](workers:
Iterable[A]) extends Actor{
def act = loop{
react{
case msg: Ready =>
case msg =>
val _sender = sender
workers foreach {_ ! Ready(this)}
react {
case Ready(worker) =>
worker.send(msg, _sender)
}
}
}
+ • роутер незамедлительно узнает, когда
появляется незанятый работник
• роутер может работать с любыми типами
актеров (локальные, удаленные)
- • сообщения о статусе работников создают
много «шума»
• роутер заблокирован, пока не освободится
какой-нибудь «работник»
Более эффективное решение в рамках одной
JVM - балансировка на уровне планировщика/
диспетчера потоков.
Akka
Пул актеров управляется work-stealing dispatcher.
Если какой-то актер из пула не занят, он «крадет»
сообщения из mailbox’а другого актера
Akka: work-stealing dispatcher
object MyActor {
val dispatcher = Dispatchers.newExecutorBasedEvent>
DrivenWorkStealingDispatcher(name).build
}
class MyActor extends Actor { self.dispatcher = MyActor.dispatcher ; ...
}
val actor1= actorOf[MyActor].start val actor2= actorOf[MyActor].start
actor1 ! LongRunningTask
actor1 ! LongRunningTask
actor2
«крадет» сообщение
Супервизор
“If somebody dies, other people
will notice” Programming Erlang, Joe Armstrong
Линки объединяют группу актеров в единую
подсистему. Если один из актеров «умирает»,
прилинкованные актеры тоже прекращают работу
actor {
self link anotherActor
…
}
Ловушка перехватывает сигнал выхода
actor {
self.trapExit = true
… }
Линки и ловушки – базовые элементы для
построения систем на основе актеров
• супервизор – любой актер, прилинкованный к
другому, и обрабатывающий сигнал выхода
• supervisor (OTP) - если контролируемый актер
умирает, перехватывает сигнал выхода, и
принимает меры, в соответствии с выбранной
стратегией
Супервизор
Доступные реализации
• Akka Supervisors
• Scala-OTP
– хороший выбор, если вы работаете с
scala.actors
SupervisorConfig(
OneForOneStrategy(List(classOf[Exception]), 3, 10),
Supervise(
myFirstActor,
Permanent) ::
Supervise(
mySecondActor,
Permanent) ::
Nil)
Akka Supervisors
Стратегия поведения в случае «смерти» актера
Наблюдаемые актеры
Вдохновлены супервизорами из Erlang/OTP
• транзакционный актер
• каталог актеров и ресурсов
• фасад процессов
• многие другие
Спасибо за внимание!
Вопросы?