Данная статья является доработанной текстовой версией одноименного доклада с конференции C++ CoreHard Autumn 2016, которая проходила в Минске в октябре прошлого года. Желание сделать эту статью возникло под впечатлением о том, что в мире C++ разработчики как бы делятся на два больших и не пересекающихся лагеря. В первом лагере находятся матерые спецы, которые все видели, все знают и все умеют, за плечами у которых десятки собственноручно написанных реализаций Модели Акторов, внутрях у которых хитрые, конечно же самостоятельно сделанные, lock-free очереди и state-of-the-art механизмы обслуживания сообщений. Такие проффи сами часами могут рассказывать про тонкости многопоточного программирования (только почему-то редко это делают). Во втором лагере — зеленые новички, которых волею судьбы занесло в мир C++, которые пока слабо представляют себе различия между unique_ptr и shared_ptr, про шаблоны только слышали, а в области многопоточности имеют поверхностное впечатление только о std::thread, std::mutex и, может быть, std::condition_variable. Для людей из первого лагеря я вряд ли что-нибудь интересное расскажу, а вот разработчикам из второго лагеря попробую вкратце рассказать о том, что Модель Акторов в C++ — это нормально. И что есть ряд готовых инструментов, на примере которых можно увидеть, что же это такое.


Введение


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


Многопоточность, как инструмент, используется в двух сильно разных направлениях. Первое — это parallel computing. Многопоточность здесь нужна для параллельного выполнения одних и тех же операций над разными блоками данных. Тем самым сильно сокращая время решения конкретной вычислительной задачи. Например, перекодирование видеофайла в один поток может занять час. А перекодирование в четыре параллельных потока — всего 15 минут.


Второе направление — это concurrent computing. Т.е. одновременное выполнение множества разных операций. Например, многопоточный сервер СУБД, который одновременно принимает запросы, строит планы их выполнения, производит операции ввода-вывода, отдает результаты запросы клиентам, обновляет статистику и т.д. Многопоточность здесь нужна для обеспечения действительно параллельного выполнения различных операций. Хотя, по большому счету, обеспечивать concurrency можно даже и на одном потоке (т.н. квазипараллелизм).


Так вот дальше речь пойдет о многопоточности применительно к concurrent computing. Ибо именно в этом направлении использование Модели Акторов полностью себя оправдывает.


Что же такого сложного в многопоточном программировании?


Одна из самых больших сложностей в многопоточности — это мутабельное разделяемое состояние.


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


Как упростить себе жизнь?


Ничего не разделять. Принцип shared nothing, который широко известен в узких кругах.


Вместо того, чтобы иметь N потоков, которые конкурируют друг с другом за ресурсы, можно сделать M потоков, каждый из которых будет владеть собственными данными. Ни один из потоков не имеет доступа к данным других потоков.


Замечательно, но что, если потоку X потребовалась какая-то информация, которая есть у потока Y? Или если поток Y хочет, чтобы поток Z обновил какие-то данные у себя?


Значит, потоки должны каким-то образом взаимодействовать между собой. Каким образом?


На первый взгляд кажется, что вариантов два:


Либо синхронно.
Либо асинхронно.


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


Остается асинхронное взаимодействие.


Подходим к Модели Акторов издалека...


Как у нас можем происходить общение рабочих потоков на сообщениях? Например, вот так:


  • поток X отсылает сообщение-запрос потоку Y;
  • поток Y когда-то получает запрос потока X и отсылает потоку X ответ;
  • поток Y отсылает сообщение-обновление потоку Z;
  • поток Z когда-то получит сообщение-обновление от потока Y и обновит те данные, которые принадлежат потоку Z.

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


Если же потоку X нужно что-то от другого потока Y, то поток X помещает сообщение во входящую очередь потока Y.


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


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


Вот мы на пальцах и показали часть основных принципов Модели Акторов.


Собственно, Модель Акторов. В двух-трех словах


Модель Акторов появилась в 1973-ем году благодаря работам Карла Хьюитта (Carl Hewitt), а затем была развита в 1981-ом году Уильямом Клингером (William Clinger) и в 1985-ом Гулом Агха (Gul Agha).


Модель Акторов несколько раз привлекала к себе широкое внимание. Последняя волна известности, по субъективному впечатлению, началась подниматься где-то лет 10-12 назад. Сначала этому способствовал язык программирования Erlang. Затем фреймворк Akka.


Желающие погрузиться в теоретическую часть Модели Акторов могут начать со следующих обзорных статей в Wikipedia и далее по ссылкам:


History of the Actor Model
Actor Model
Actor Model Theory


Однако, погружение в теорию Модели Акторов — это чистой воды прыжок в Computer Science. Но я не ученый, а инженер-программист в прошлом, менеджер в нынешнем, поэтому позволю себе сконцентрироваться только на практических аспектах.


Модель Акторов «на пальцах»


Если не вдаваться в скучную формальную теорию, то Модель Акторов базируется на следующих принципах:


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

Принципы простые. И, когда к ним привыкнешь, очевидные. Тем не менее, один важный момент нужно затронуть отдельно. Этот момент очень важен, т.к. он поясняет, почему реализации Модели Акторов могут выглядеть по-разному и очень сильно отличаются друг от друга.


Больше акторов, хороших и разных!


Актор — это некоторая сущность.


Модель акторов не говорит о том, как именно эта сущность должна быть реализована.


Актором может быть отдельный процесс. Например, так происходит в Erlang, где каждый легковесный процесс внутри Erlang VM может считаться актором.


Актором может быть отдельный поток (OS thread, «green» thread, fiber, etc...). Например, goroutines в языке Go так же можно рассматривать как акторы (с натяжкой).


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


С чем же ассоциируется Модель Акторов сейчас?


Erlang


Прежде всего — это Erlang.


Ирония в том, что я никогда не встречал, чтобы Джо Армстронг говорил, что на Erlang повлияла Модель Акторов.

Erlang сам по себе. А так же попытки создать более удобные для разработчиков языки на базе Erlang VM. Например, Elixir.


История Erlang началась в далеком 1986-м году в одной из лабораторий компании Ericsson. Джо Армстронг экспериментировал с Prolog-ом для написания программ для телефонии. В результате этих экспериментов появился Erlang.


В 1995-ом году в Ericsson-е была закрыта неудачная разработка нового телефонного свича AXE-N (кстати, на C++). В новой разработке в качестве основного языка использовался Erlang. Итогом стал успешный программно-аппаратный продукт AXD301, внутри которого было порядка миллиона(!) строк кода на Erlang.


Правда далее история Erlang-а в Ericsson-е развивалась парадоксальным образом. Вскоре после создания AXD301 использование Erlang-а в разработке новых продуктов внутри Ericsson-а было запрещено. Джо Армстронг ушел из Ericsson-а, основал свою компанию, язык Erlang вышел в OpenSource.


Через несколько лет, когда Erlang доказал свою состоятельность находясь в «свободном» плавании, запрет в Ericsson-е был снят. В 2004-ом году Армстронг возвращается в Ericsson.


За последние 15 лет Erlang доказал свою состоятельность более чем убедительно.
Огромное количество продуктов разработано на Erlang, ряд компаний использует Erlang в качестве ключевого инструмента.


Например, WhatsApp.


Многие компании убеждаются в преимуществах Erlang-а и начинают использовать его у себя. Например, Wargaming сформировал у себя серьезную команду Erlang-разработчиков (возможно, самую серьезную в СНГ) и потихоньку переучивает Python-истов на Erlang.


Давайте посмотрим маленький пример простейшей программы на Erlang-е. Классический для Модели Акторов пример — «пинг-понг»:


-module(tut15).
-export([start/0, ping/2, pong/0]).

ping(0, Pong_PID) ->
    Pong_PID ! finished,
    io:format("ping finished~n", []);

ping(N, Pong_PID) ->
    Pong_PID ! {ping, self()},
    receive
        pong ->
            io:format("Ping received pong~n", [])
    end,
    ping(N - 1, Pong_PID).

pong() ->
    receive
        finished ->
            io:format("Pong finished~n", []);
        {ping, Ping_PID} ->
            io:format("Pong received ping~n", []),
            Ping_PID ! pong,
            pong()
    end.

start() ->
    Pong_PID = spawn(tut15, pong, []),
    spawn(tut15, ping, [3, Pong_PID]).

Akka


Вторая по известности «икона» Модели Акторов — это фреймворк Akka для языков Scala и Java.


Историю Akka можно начать с 2006-го года, когда Филипп Холлер (Philipp Haller) разработал реализацию Модели Акторов для языка Scala. Эта реализация вошла в стандартную библиотеку Scala.


Через несколько лет, когда Scala и акторы из ее стандартной библиотеки доказали свою состоятельность, Джонес Бонер (Jonas Boner) в 2008-м приступил к созданию фреймворка Akka, первая публичная версия которого вышла в 2010-ом. Одним из существенных отличий Akka от акторов из стандартной библиотеки Scala стало то, что Akka поддерживала как Scala, так и Java. В этом смысле примечательно то, что компания Lightbend (бывший TypeSafe), которая стоит за разработкой Akka и оказывает коммерческую поддержку Akka, заявила о сдвиге своего фокуса со Scala в пользу Java.


Akka широко используется в области Web-а и онлайн-сервисов (например, Twitter, LinkedIn). Люди, стоящие за Akka, причастны к таким современным buzz-word-ам, как Reactive Manifesto и Microservices.


Ну и для того, чтобы составить впечатление о том, как выглядит код с использованием Akka, одна из реализаций «пинг-понга» для Akka на Scala (таких реализаций множество, конкретно эта найдена здесь):


import akka.actor._

case object PingMessage
case object PongMessage
case object StartMessage
case object StopMessage

class Ping(pong: ActorRef) extends Actor {
  var count = 0
  def incrementAndPrint { count += 1; println("ping") }
  def receive = {
    case StartMessage =>
        incrementAndPrint
        pong ! PingMessage
    case PongMessage => 
        incrementAndPrint
        if (count > 99) {
          sender ! StopMessage
          println("ping stopped")
          context.stop(self)
        } else {
          sender ! PingMessage
        }
  }
}

class Pong extends Actor {
  def receive = {
    case PingMessage =>
        println("  pong")
        sender ! PongMessage
    case StopMessage =>
        println("pong stopped")
        context.stop(self)
  }
}

object PingPongTest extends App {
  val system = ActorSystem("PingPongSystem")
  val pong = system.actorOf(Props[Pong], name = "pong")
  val ping = system.actorOf(Props(new Ping(pong)), name = "ping")
  // start them going
  ping ! StartMessage
}

В чем сила, брат?


Почему же Erlang, Akka и другие похожие на них инструменты получили такую популярность?


Тут уместно было бы привести цитату из Джо Армстронга (создателя языка Erlang):


I also suspect that the advent of true parallel CPU cores will make programming parallel systems using conventional mutexes and shared data structures almost impossibly difficult, and that the pure message-passing systems will become the dominant way to program parallel systems.

Что в грубом пересказе, но с сохранением смысла сказанного, может звучать как:


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


Отмечу несколько ключевых факторов, которые объясняют успех Erlang и Akka:


  • простота разработки. Использование асинхронного обмена сообщениями сильно упрощает жизнь когда приходится иметь дело с concurrent computing;
  • масштабирование. Модель Акторов позволяет создавать огромное количество акторов, каждый из которых отвечает за свою частную задачу. Принцип shared nothing и асинхронный обмен сообщениями позволяет строить распределенные приложения, горизонтально масштабируясь по мере надобности;
  • отказоустойчивость. Сбой одного актора может отлавливаться другими акторами, которые предпринимают соответствующие действия для восстановления ситуации (например, механизм супервизоров из Erlang-а).

Но это все безопасные языки и управляемые среды.


И, кстати говоря, вопросы отказоустойчивости и безопасных языков, которые работают в управляемых средах, довольно сильно увязаны друг с другом. Так, если внутри Erlang-овой VM какой-то легковесный процесс выполнит деление на ноль, то Erlang VM просто закроет один этот процесс и на работоспособность других процессов это не скажется. Однако, если мы возьмем многопоточное приложение на C++, в одном из потоков которого происходит деление на ноль, то аварийно завершит работу все приложение.


Так что в том, что в последние годы Модель Акторов стала популярна в первую очередь в безопасных языках, есть свои объективные факторы.


А есть ли смысл в реализациях Модели Акторов для языка C++?


Чтобы ответить на этот вопрос, нужно сперва ответить на другой вопрос: а нужен ли сейчас вообще C++?


Язык C++ — это старый язык с очень длинной историей. Даже если считать от момента официального релиза (осень 1985-го), то ему уже больше тридцати лет. Само название C++ появилось в 1983-ем, а работа над языком началась в 1979-ом. Т.е. скоро можно будет говорить о сорокалетней истории языка.


За это время С++ вобрал в себя множество новшеств и заимствований. Но сохранил, при этом, совместимость с изрядным подмножеством языка C.


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


Использовать C++ сложно. C++ часто критикуют. Очень часто критикуют заслуженно.


Развитие таких языков, как Java/Scala/C# с одной стороны, рост популярности функциональных языков (вроде OCaml и Haskell) с другой, а также появление новых и «современных» альтернатив, вроде Go, вытеснило C++ из многих прикладных ниш, в которых он волею судьбы оказался в 1980-х и 1990-х годах.


Поэтому последние лет 15-ть язык C++ регулярно хоронят. Я сам лет 8-мь назад всерьез считал, что у C++ нет будущего.


Тем не менее, C++ здесь. Жив-здоров. Успешно развивается. Становится еще большим монстром, чем был. Но, что удивительно, чем более сложным языком C++ становится, тем проще его использовать в повседневной работе.


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


Этот язык — C++.


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


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

Так что хотим мы того или нет, но C++ пока что здесь. И, по всей видимости, будет здесь еще долго.


А раз так, и раз на C++ разрабатываются большие и сложные программы, в том числе и с использованием многопоточности, то почему бы не упростить себе жизнь за счет применения Модели Акторов?


Что есть готового для C++?


Давайте посмотрим, что для C++ есть готового из реализаций Модели Акторов. Дабы не переизобретать велосипед и иметь возможность взять что-то существующее, вместо того, чтобы дать что-то свое с нуля. Ну или убедиться в том, что нужной для вас реализации нет и имеет смысл убить пару-тройку человеко-лет на создание еще одного решения.


Вообще-то говоря, готовых реализаций Модели Акторов не так уж много. Один из списков можно найти в Wikipedia: Actor Libraries and Frameworks. Но там, к сожалению, перечислено не все и часть проектов уже не подает признаков жизни.


Ниже мы рассмотрим несколько реализаций, которые явно живы, здоровы, не просто подают признаки жизни, но и эволюционно развиваются. Кроме того, эти проекты могут похвастаться переносимостью между разными платформами. Например, по этой причине в обзор не включена Asynchonous Agents Library от Microsoft, которая доступна в Microsoft Visual Studio. Так же в обзор не попали OOSMOS (заточенность в первую очередь под чистый C, а не под C++) и actor-zeta (пока еще находящийся на ранней стадии своего развития).


QP/C++


Начнем с библиотеки QP/C++.


QP/C++ — это зрелый (более 15 лет развития) программный продукт под двойной лицензией, предназначенный для разработки встраиваемого ПО, в том числе и систем реального времени. В том числе и систем, которые могут работать прямо на голом железе. В том числе QP/C++ частично соответствует MISRA C++2008. Из всего этого и проистекает его специфичность. Так же это единственный фреймворк в обзоре, которому достаточно C++98.


Акторы в QP/C++ называются активными объектами и представляю из себя иерархические конечные автоматы. Код акторов можно набирать в виде обычных C++ных классов. А можно нарисовать актора в специальном инструменте для визуального моделирования и его код будет сгенерирован автоматически.


Активные объекты в QP/C++ работают на контексте, который им выделяет QP. В зависимости от окружения активные объекты могут работать каждый на своей нити или же они могут разделять общий рабочий контекст.


В качестве иллюстрации посмотрим на один из примеров из состава QP, в котором заставляют периодически мигать светодиод на каком-то устройстве.


Исходный файл blinky.h, в котором декларируется актор и все, что с ним связано:

#ifndef blinky_h
#define blinky_h

using namespace QP;

enum BlinkySignals {
    DUMMY_SIG = Q_USER_SIG,
    MAX_PUB_SIG,  // the last published signal

    TIMEOUT_SIG,
    MAX_SIG       // the last signal
};

extern QMActive * const AO_Blinky; // opaque pointer

#endif // blinky_h

Файл main.cpp, в котором инициируется работа актора:


#include "qpcpp.h"
#include "bsp.h"
#include "blinky.h"

int main() {
    static QEvt const *blinkyQSto[10]; // Event queue storage for Blinky

    BSP_init(); // initialize the Board Support Package
    QF::init(); // initialize the framework and the underlying RT kernel

    // instantiate and start the active objects...
    AO_Blinky->start(1U,                            // priority
                 	blinkyQSto, Q_DIM(blinkyQSto), // event queue
                 	(void *)0, 0U);                // stack (unused)

    return QF::run(); // run the QF application
}

Ну и файл blinky.cpp, в котором актор реализован:


#include "qpcpp.h"
#include "bsp.h"
#include "blinky.h"

class Blinky : public QActive {
private:
    QTimeEvt m_timeEvt;

public:
    Blinky();

protected:
    static QState initial(Blinky * const me, QEvt const * const e);
    static QState off(Blinky * const me, QEvt const * const e);
    static QState on(Blinky * const me, QEvt const * const e);
};

Blinky l_blinky;

QMActive * const AO_Blinky = &l_blinky; // opaque pointer

Blinky::Blinky()
  : QActive(Q_STATE_CAST(&Blinky::initial)),
    m_timeEvt(this, TIMEOUT_SIG, 0U)
{}

QState Blinky::initial(Blinky * const me, QEvt const * const e) {
    (void)e; // unused parameter

    // arm the time event to expire in half a second and every half second
    me->m_timeEvt.armX(BSP_TICKS_PER_SEC/2U, BSP_TICKS_PER_SEC/2U);
    return Q_TRAN(&Blinky::off);
}

QState Blinky::off(Blinky * const me, QEvt const * const e)
{
    QState status;
    switch (e->sig) {
        case Q_ENTRY_SIG: {
            BSP_ledOff();
            status = Q_HANDLED();
            break;
        }
        case TIMEOUT_SIG: {
            status = Q_TRAN(&Blinky::on);
            break;
        }
        default: {
            status = Q_SUPER(&QHsm::top);
            break;
        }
    }
    return status;
}

QState Blinky::on(Blinky * const me, QEvt const * const e)
{
    QState status;
    switch (e->sig) {
        case Q_ENTRY_SIG: {
            BSP_ledOn();
            status = Q_HANDLED();
            break;
        }
        case TIMEOUT_SIG: {
            status = Q_TRAN(&Blinky::off);
            break;
        }
        default: {
            status = Q_SUPER(&QHsm::top);
            break;
        }
    }
    return status;
}

Just::Thread Pro: Actors Edition


Следующий инструмент — Just::Thread Pro: Actors Edition.


Платная библиотека от очень известного в C++ном мире Энтони Уильямса (Anthony Williams). Автора книги «C++ Concurrency in Action».


Собственно, достоинства библиотеки на этом и заканчиваются :)


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


В качестве иллюстрации посмотрим на классический пример ping-pong.


#include <jss/actor.hpp>
#include <iostream>
#include <thread>

int main()
{
    struct pingpong {
        jss::actor_ref sender;

        pingpong(jss::actor_ref sender_): sender(sender_) {}
    };
    jss::actor pp1( 
        []{
            for(;;)
            {
                jss::actor::receive().match<pingpong>(
                    [](pingpong p){
                        std::cout<<"ping\n";
                        p.sender.send(pingpong(jss::actor::self()));
                    });
            }
        });

    jss::actor pp2(
        []{
            for(;;)
            {
                jss::actor::receive().match<pingpong>(
                    [](pingpong p){
                        std::cout<<"pong\n";
                        p.sender.send(pingpong(jss::actor::self()));
                    });
            }
        });

    pp1.send(pingpong(pp2));

    std::this_thread::sleep_for(std::chrono::seconds(2));
    pp1.stop();
    pp2.stop();
}

C++ Actor Framework


Следующий инструмент — это C++ Actor Framework. Он же CAF, он же libcppa в недавном прошлом.


OpenSource проект под BSD-лицензией.


Если можно говорить о самой известной реализации Модели Акторов для C++, то это про CAF. Пожалуй, более распиаренной библиотеки на эту тему для C++ нет.


CAF копирует Erlang в C++ настолько близко, насколько это возможно. Поэтому если вы знаете Erlang, вам нравится Erlang, но вам нужно вести разработку на C++ и вы хотели бы писать на C++ как на Erlang, то вам прямиком в CAF.


Ценой за мимикрию под Erlang являются высокие требования CAF-а к уровню поддержки стандартов в C++ в компиляторе. Из-за этого разработчики CAF-а никогда не рассматривали Windows и VC++ в качестве одной из значимых платформ для своей разработки, ограничиваясь Linux-ом, FreeBSD MacOS, а также самыми свежими версиями компиляторов gcc и clang. Кроме того, авторы CAF несколько раз заявляли, что они и впредь будут ориентироваться прежде всего на самые новые возможности языка C++ и будут переходить на фичи из новых стандартов так быстро, как это возможно.


Отметим, что CAF предлагает готовые инструменты для создания распределенных приложений. Для этого в CAF есть свой протокол для общения удаленных агентов и реализация этого протокола посредством Boost::Asio.


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


Кроме того, за те два с половиной года, что CAF маячит в поле моего зрения, уже несколько раз заявлялось о нарушении совместимости при выпуске новых версий CAF-а.


Ну и разработчики CAF-а позиционируют его как очень шустрый фреймворк, хотя на этот счет у некоторых есть обоснованные сомнения ;)


В качестве иллюстрации можно привести код примера fixed_stack из состава самого CAF-а.


#include <cassert>
#include <cstdint>
#include <iostream>
#include "caf/all.hpp"

using std::endl;
using namespace caf;

namespace {

using pop_atom = atom_constant<atom("pop")>;
using push_atom = atom_constant<atom("push")>;

enum class fixed_stack_errc : uint8_t { push_to_full = 1, pop_from_empty };

error make_error(fixed_stack_errc x) {
  return error{static_cast<uint8_t>(x), atom("FixedStack")};
}

class fixed_stack : public event_based_actor {
public:
  fixed_stack(actor_config& cfg, size_t stack_size)
      : event_based_actor(cfg),
        size_(stack_size)  {
    full_.assign(
      [=](push_atom, int) -> error {
        return fixed_stack_errc::push_to_full;
      },
      [=](pop_atom) -> int {
        auto result = data_.back();
        data_.pop_back();
        become(filled_);
        return result;
      }
    );

    filled_.assign(
      [=](push_atom, int what) {
        data_.push_back(what);
        if (data_.size() == size_)
          become(full_);
      },
      [=](pop_atom) -> int {
        auto result = data_.back();
        data_.pop_back();
        if (data_.empty())
          become(empty_);
        return result;
      }
    );

    empty_.assign(
      [=](push_atom, int what) {
        data_.push_back(what);
        become(filled_);
      },
      [=](pop_atom) -> error {
        return fixed_stack_errc::pop_from_empty;
      }
    );
  }

  behavior make_behavior() override {
    assert(size_ < 2);
    return empty_;
  }

private:
  size_t size_;
  std::vector<int> data_;
  behavior full_;
  behavior filled_;
  behavior empty_;
};

void caf_main(actor_system& system) {
  scoped_actor self{system};
  auto st = self->spawn<fixed_stack>(5u);
  // fill stack
  for (int i = 0; i < 10; ++i) self->send(st, push_atom::value, i);
  // drain stack
  aout(self) << "stack: { ";
  bool stack_empty = false;
  while (!stack_empty) {
    self->request(st, std::chrono::seconds(10), pop_atom::value).receive(
      [&](int x) {
        aout(self) << x << "  ";
      },
      [&](const error&) {
        stack_empty = true;
      }
    );
  }
  aout(self) << "}" << endl;
  self->send_exit(st, exit_reason::user_shutdown);
}
} // namespace <anonymous>
CAF_MAIN()

SObjectizer


Ну и четвертый фреймворк, на котором мы остановимся чуть подробнее — это SObjectizer.


OpenSource-проект под лицензией BSD.


Проект развивается с 2002-го года, хотя базируется на идеях, которые были выработаны и проверены еще в середине 90-х при разработке небольшой объектно-ориентированной SCADA-системы (развитие которой, к сожалению, завершилось в 2000-ом).


SObjectizer никогда не были экспериментальным проектом, он создавался специально для того, чтобы упростить разработку многопоточного софта на C++. До сих пор в эксплуатации находятся программные системы, написанные на разных версиях SObjectizer.


Поэтому в SObjectizer огромное внимание уделяется совместимости. Например, осенью 2014-го года вышла версия SObjectizer-5.5.0. С тех пор в рамках версии 5.5 прошло более двадцати релизов, последняя стабильная версия имеет номер 5.5.18, но ломающих изменений не было. Так что SObjectizer — это проект с весьма долгой историей и трепетным отношением к совместимости между версиями.


Акторы в SObjectizer называются агентами. Просто по историческим причинам.


Как и в QP/C++ агенты в SObjectizer — это, как правило, экземпляры отдельных C++ных классов. Так же, как и в QP/C++ агенты представляют из себя иерархические конечные автоматы (включая вложенные состояния, deep- и shallow-историю, обработчики входа-выхода, временные лимиты).


Так же, как и в QP/C++ рабочий контекст агентам предоставляет фреймворк. Для этого в SObjectizer есть такое понятие, как диспетчер: специальная сущность, которая выполняет диспетчеризацию событий агентов. В состав SObjectizer входит восемь типов диспетчеров, доступных разработчику «из коробки». Среди них есть такой интересный диспетчер, как adv_thread_pool, который позволяет параллельно запускать на разных рабочих нитях обработчики событий одного и того же агента, если эти обработчики помечены как thread-safe.


В чем SObjectizer сильно отличается от перечисленных выше проектов, так это симбиозом моделей Акторов, Publish-Subscribe и Comminicating Sequential Processes.


В SObjectizer сообщения отсылаются не напрямую агентам-получателям, а в mbox-ы (почтовые ящики). А уже из mbox-а сообщения доставляются тем агентам, которые на него подписаны. Таким образом mbox-ы в SObjectizer работают как Topic-и в модели Publish-Subscribe. Отсылка сообщения в mbox — это как операция Publish. Агенты же должны выполнить операцию Subscribe для получения интересующих их сообщений.


Вот чего сейчас SObjectizer не предоставляет, так это готовых средств построения распределенных приложений. Подобные средства были в ранних версиях SObjectizer-а, но со временем по ряду объективных причин от них отказались и начиная с 2010-го года подобных инструментов в ядре SObjectizer-а нет. Пользователь сам выбирает, какой коммуникационный слой ему удобнее использовать — будь это REST, MQTT, CoAP, AMQP или что-то еще.


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


#include <iostream>

#include <so_5/all.hpp>

class fixed_stack final : public so_5::agent_t
{
  state_t st_empty{ this },
          st_filled{ this },
          st_full{ this };
 
  const size_t m_max_size;
  std::vector< int > m_stack;
 
public :
  class empty_stack final : public std::logic_error
  {
  public :
    using std::logic_error::logic_error;
  };

  struct push { int m_val; };
  struct pop : public so_5::signal_t {};

  fixed_stack( context_t ctx, size_t max_size )
    : so_5::agent_t( ctx )
    , m_max_size( max_size )
  {
    this >>= st_empty;
 
    so_subscribe_self()
      .in( st_empty )
      .in( st_filled )
      .event( &fixed_stack::on_push );
 
    so_subscribe_self()
      .in( st_filled )
      .in( st_full )
      .event( &fixed_stack::on_pop_when_not_empty );
 
    so_subscribe_self()
      .in( st_empty )
      .event( &fixed_stack::on_pop_when_empty );
  }

private :
  void on_push( const push & w )
  {
    m_stack.push_back( w.m_val );
    this >>= ( m_stack.size() == m_max_size ? st_full : st_filled );
  }
 
  int on_pop_when_not_empty( mhood_t< pop > )
  {
    auto r = m_stack.back();
    m_stack.pop_back();
    this >>= ( m_stack.empty() ? st_empty : st_filled );
    return r;
  }
 
  int on_pop_when_empty( mhood_t< pop > )
  {
    throw empty_stack( "empty_stack" );
  }
};  

int main() {
  try {
    so_5::launch( []( so_5::environment_t & env ) {
      so_5::mbox_t stack;
      env.introduce_coop( [&stack]( so_5::coop_t & coop ) {
        stack = coop.make_agent< fixed_stack >( 5u )->so_direct_mbox();
      } );

      for( int i = 0; i < 10; ++i )  so_5::send< fixed_stack::push >( stack, i );

      std::cout << "stack { ";
      try {
        for(;;)
          std::cout << so_5::request_value< int, fixed_stack::pop >( stack, std::chrono::seconds(10) ) << " ";
      }
      catch( const fixed_stack::empty_stack & ) {}
      std::cout << "}" << std::endl;

      env.stop();
    } );
    return 0;
  }
  catch( const std::exception & x )  {
    std::cerr << "Oops! " << x.what() << std::endl;
  }
  return 2;
}

Заключение


Модель Акторов — это очень удобный инструмент в случаях, где использование этой модели уместно. Это уже неоднократно доказывалось успешным применением таких инструментов, как Erlang и Akka в самых разнообразных проектах. Да и вообще за асинхронным обменом сообщений между независимыми сущностями будущее. Прислушайтесь к Джо Армстронгу, он плохого не посоветует.


Но не верьте рекламе: использование Модели Акторов уместно не всегда.


Наш опыт показывает, что при наличии подходящих инструментов Модель Акторов имеет смысл применять и в C++. При этом для C++ готовые инструменты уже есть. На разный вкус и цвет.


И размер кошелька, конечно же. В коммерческом проекте за QP/C++ и за Just::Thread Pro придется заплатить. За SObjectizer и CAF — нет. По крайней мере сразу не придется.


Вот чего делать не стоит, так это браться за написание собственного акторного фреймворка.


Неблагодарное это дело. Проверено на людях. Лучше все-таки взять что-то готовое.


И пусть кто-нибудь другой весело бегает по граблям многопоточности :)

Поделиться с друзьями
-->

Комментарии (29)


  1. quietp
    21.02.2017 16:04

    Скажите, а messaging frameworks, такие как например zeroMQ, рассматриваются в качестве implementation of Actor Model?


    1. eao197
      21.02.2017 16:12

      Хм… Никогда не задумывался на эту тему. Насколько я помню, при работе с ZeroMQ нужно самостоятельно извлекать сообщения из канала, определять их тип и искать подходящий обработчик для сообщения. Т.е. приходится вручную делать то, что акторный фреймворк делает автоматически. С этой точки зрения я бы не стал причислять ZeroMQ к реализациям Модели Акторов.

      А вот в TIBCO Rendezvous, емнип, брала на себя заботы не только по доставке сообщений через транспортные каналы, но и по диспетчеризации листенеров сообщений. В этом плане листенеры в TIBCO Rendezvous похожи на акторов, а сам TIBCO Rendezvous может рассматриваться одна из возможных реализаций Модели Акторов. Хотя TIBCO Rendezvous больше относится к модели Publish/Subscribe, имхо.


  1. skorokhod
    21.02.2017 21:41
    +1

    CAF вполне себе нормально компилируется под msvc и требует лишь c++11, поэтому говорить о каком-то игнорировании платформы некорректно — требуется соответствие стандарту.


    1. eao197
      21.02.2017 21:43

      А разве было сказано, что CAF не компилируется msvc?

      Нормально он стал компилироваться после выхода VC 14.0, до этого под Windows они рекомендовали пользоваться MinGW.


      1. skorokhod
        21.02.2017 21:57
        +1

        высокие требования CAF-а к уровню поддержки стандартов в C++ в компиляторе. Из-за этого разработчики CAF-а никогда не рассматривали Windows и VC++ в качестве одной из значимых платформ для своей разработки, ограничиваясь Linux-ом, FreeBSD MacOS, а также самыми свежими версиями компиляторов gcc и clang.


        Высокие требования — это c++11, не более. Появилась поддержка c++11 — появилась поддержка платформы. В 2017 году говорить об этом, как о некой чрезвычайной требовательности, странно.


        Посмотрете на историю добавления optional и variant типов — авторы добавили их с оглядкой на совместимость с c++17, но не потребовали поддержки "в новейших компиляторах".


        1. eao197
          21.02.2017 22:06

          Ну и где речь идет об «игнорировании платформы»? Значимой платформой Windows для них не была, они сами об этом, по-моему, в своем блоге писали. Мол, чем тратить время на борьбу с заморочками msvs, мы лучше сделаем что нам нужно на самых свежих версиях gcc и clang.

          Что до 2017-го года, то, к сожалению, еще есть проекты, которые пока еще и не думают на vc 14.0 переходить, хорошо, если хотя бы на vc 12.0 сидят, а то могут и на более старых компиляторах оставаться.

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


  1. begemot_sun
    21.02.2017 21:49

    А почему это не в блоге Erlang/OTP? Можно было бы добавить туда тоже.


    1. eao197
      21.02.2017 21:50

      Наверное потому, что я не в курсе, что за блог Erlang/OTP.


      1. begemot_sun
        21.02.2017 22:22

        Ну у вас там треть статьи про Erlang и всякие его VM. И даже код приведен. Неспроста же?


        1. eao197
          21.02.2017 22:24

          Говорить сейчас про Модель Акторов и не упомянуть про Erlang просто нельзя.
          Но вот в какой блог нужно было разместить данную статью я так и не понял.


          1. begemot_sun
            21.02.2017 22:48

            По молодежному сейчас это называется хаб:
            https://habrahabr.ru/hub/erlang/


            1. eao197
              21.02.2017 23:03

              А, понятно.
              Боюсь, там это будет все-таки оффтопик.


  1. skorokhod
    21.02.2017 22:03

    Хорошо бы добавить, что CAF поддерживает несколько различных типов обмена сообщениями:
    Обычная отправка адресату, pub/sub (через группы), а также req/rep.


    Кроме того, 0.15 получил возможность типизировать акторы, указывая типы обрабатываемых запросов/ответов, с проверкой в compile-time (невозможно отправить актору сообщение неподдерживаемого типа)


    1. eao197
      21.02.2017 22:14

      Было бы хорошо, если бы тот, кто имеет положительный опыт использования CAF-а, рассказал бы об этом фреймворке русскоязычным читателям в отдельной статье.


      1. skorokhod
        21.02.2017 22:54
        +1

        Текст претендует на некоторое сравнение существующих фреймфорков, и заканчивается фразой:

        В чем SObjectizer сильно отличается от перечисленных выше проектов, так это симбиозом моделей Акторов, Publish-Subscribe и Comminicating Sequential Processes.

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

        p.s.
        Всё же написание статьи требует на порядок больше времени и сил, чем комментарий.


        1. eao197
          21.02.2017 23:07

          > Текст претендует на некоторое сравнение существующих фреймфорков

          Вам показалось, не претендует.

          > Но при этом не содержит упоминания о реализуемых моделях в других реализациях.

          Про механизм групп в CAF у меня вообще не было представления, мне казалось, они там вообще для каких-то других целей использовались. Может быть это какое-то свежее добавление в 0.15.

          CAF-ом никогда не пользовался, приходилось на него смотреть изредка, когда вот с подобными комментариями сталкивался. Что знал, рассказал. У кого есть опыт, пожалуйста, делитесь. Наверняка получится интереснее и подробнее, чем у меня.


  1. Deosis
    22.02.2017 09:07

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


    1. begemot_sun
      22.02.2017 09:19

      Ну да: https://habrahabr.ru/post/219201/


    1. eao197
      22.02.2017 09:36

      Не акторы, а акторный фреймворк. Как раз это один из факторов, благодаря которому разработка многопоточного софта с использованием акторных фреймворков оказывается проще, чем на «голых» нитях/мутексах. Программист берет готовый, отлаженный и опробированный в разных проектах инструмент и получает работающие mpsc/mpmc очереди «из коробки». Ему не нужно очереди программировать вручную, отлаживать их и т.д.

      Причем этот бонус разработчик получает не только в случае использования акторных фреймворков, но и в случае использования фреймворков с реализации других подходов к решению проблем concurrent computing: будь то реализация модели Pub/Sub или CSP. Но акторные фреймворки, особенно те, в которых актор — это не поток ОС, а объект или короутина, дают еще одно преимущество: они берут на себя задачу диспетчеризации событий для акторов. Тем самым снимая с разработчика еще и заботы об организации рабочих потоков, распределение задач по ним и т.д.


    1. skorokhod
      22.02.2017 12:06
      +1

      Есть один существенный нюанс. При «небольшом» потоке сообщений и «быстрой» обработке, крайне невыгодно помещать генераторы и обработчики в свои индивидуальные потоки: расходы на переключение контекстов будут огромными. Акторный фреймворк может как взять на себя решение задачи по распределению задач по потокам, так и позволить разработчику быстро переконфигурировать приложение, указывая рспределение акторов по потокам.

      В caf существует 3 типа акторов: event_based_actor, blocking_actor и scoped_actor.

      • event_based_actor только лишь обрабатывает сообщения (в том числе, может слать сообщения сам себе ). Для такого типа акторов caf учитывает «родство» и по умолчанию помещает потомка в поток родителя. При создании актора можно задать опцию «detached», что заставит caf поместить его в свой отдельный поток
      • blocking_actor живёт в своём собственном потоке и пользователь сам решает когда он должен принимать сообщения, а когда — спать
      • scoped_actor нужен для взаимодействия с системой акторов «извне».


      Ну и да, каждый актор имеет свои собственные очереди сообщений и ожиданий ответов.


      1. eao197
        22.02.2017 12:17

        Я вам больше скажу :)
        Ключевым фактором является «тяжесть» обработки сообщения. Чем короче время обработки, тем меньше выгод в разнесении обработчиков по разным потокам. Даже при высокой интенсивности потока сообщений.

        При очень высокой интенсивности и совсем дешевой обработке, вообще актуальность использования Модели Акторов под вопросом. Тут может лучше работать подход на основе Disruptor-а.

        Что касается управления контекстами, то сейчас в SObjectizer восемь типов готовых диспетчеров и разработчик может распределять своих акторов между ними как ему захочется, в том числе выбирая конфигурацию прямо в run-time. Чему способствует так же и то, что в SObjectizer всего один вид агентов, а не зоопарк, как в CAF :)

        Ну и на счет зоопарка в CAF. Документация говорит, что там есть где развернуться:

        CAF provides several actor implementations, each covering a particular use case. The available implementations differ in three characteristics: (1) dynamically or statically typed, (2) class-based or function-based, and (3) using asynchronous event handlers or blocking receives.

        И, скажем, Pub/Sub на базе CAF-овских групп доступен для dynamically typed акторов, но не для statically typed.

        Вероятно, кому-то доставляет удовольствие со всем этим разбираться :)


        1. skorokhod
          22.02.2017 13:07
          +1

          При очень высокой интенсивности и совсем дешевой обработке, вообще актуальность использования Модели Акторов под вопросом. Тут может лучше работать подход на основе Disruptor-а.

          слишком мутное описание, чтобы из него можно было что-то почерпнуть об алгоритме.


          Ну и на счет зоопарка в CAF. Документация говорит, что там есть где развернуться:
          CAF provides several actor implementations, each covering a particular use case. The available implementations differ in three characteristics: (1) dynamically or statically typed, (2) class-based or function-based, and (3) using asynchronous event handlers or blocking receives.


          1. Есть ли в SObjectizer статическая типизация агентов? Как это сделано у Вас?
          2. Подразумевается, что актор может быть как функцией, так и классом. К чему требовать классы там, где достаточно лябды?
          3. "зоопарк" типов акторов вроде как объективен:
            • не всегда возможно запихнуть всё приложение в модель акторов (gui, например), поэтому необходим тип акторов, который живёт "вовне" (scoped_actor) и управляется внешним event-loop
            • Для работы внутри системы акторов также вохможны 2 ситуации:
              — работа, основанная на сообщениях от других акторов (event_based_actor)
              — работа, требующая ручного управления событиями внутри актора (сокеты, железо, бд и тп) (blocking_actor)

          Как решаеются эти задачи в SObjectizer?


          Вероятно, кому-то доставляет удовольствие со всем этим разбираться :)

          В SObjectizer настолько прост, что в нём ненужно разбираться?


          1. eao197
            22.02.2017 15:27

            слишком мутное описание, чтобы из него можно было что-то почерпнуть об алгоритме.

            Так описания алгоритма и не было. Речь о том, что когда идет интенсивный поток заявок, а обработка каждой заявки занимает очень небольшое время, то на практике очень хорошо зарекомендовал себя подход Disruptor.
            Есть ли в SObjectizer статическая типизация агентов? Как это сделано у Вас?

            Не понятно, что подразумевается под статической типизацией. Посмотрите пример из статьи. Метод fixed_stack::on_push будет вызван только для сообщения push, ни для какого другого сообщения его использовать не получится. Возможно это как раз посредством статической типизации.

            Вероятно, вы под статической типизацией понимаете что-то другое. Например, невозможность отправить агенту сообщение типа A, если агент в нем не заинтересован. Если так, то этого в SObjectizer нет. Поскольку на практике с такими задачами не встречались. Где это может потребоваться?

            Подразумевается, что актор может быть как функцией, так и классом. К чему требовать классы там, где достаточно лябды?

            Прекрасно, нет проблем. Можно так:
            class onliner_demo final : public so_5::agent_t {
            public :
              onliner_demo(context_t ctx) : so_5::agent_t(std::move(ctx)) {
                so_subscribe_self()
                  .event( &onliner_demo::event_A )
                  .event( &onliner_demo::event_B );
              }
            private :
              void event_A(const A &) {...}
              void event_B(const B &) {...}
            };
            

            Можно так:
            auto coop = env.create_coop("demo");
            auto onliner_demo = coop.define_agent();
            onliner_demo.event( onliner_demo, [](const A &) {...});
            onliner_demo.event( onliner_demo, [](const A &) {...});
            

            Причем для самого SO-5 разницы между этими агентами никакой нет, поскольку во втором случае неявным образом конструируется обычный наследник agent_t.

            Соответственно, вне зависимости от вида оформления агента ему доступны все, что доступно агенту (подписки, привязка к диспетчерам и т.д.).
            не всегда возможно запихнуть всё приложение в модель акторов (gui, например), поэтому необходим тип акторов, который живёт «вовне» (scoped_actor) и управляется внешним event-loop

            Тут непонятно зачем такие сущности обзывать акторами и делать для них какие-то специальные варианты. Либо есть актор, который привязывается к соответствующему диспетчеру (а диспетчер можно сделать так, чтобы он работал на GUI-нити или на другой нити со своим event-loop-ом). Либо это вообще не актор, а просто внешняя сущность, которой нужно тем или иным образом с акторами взаимодействовать. Опять же, это решается без выделения особых типов акторов.
            Для работы внутри системы акторов также вохможны 2 ситуации:
            — работа, основанная на сообщениях от других акторов (event_based_actor)
            — работа, требующая ручного управления событиями внутри актора (сокеты, железо, бд и тп) (blocking_actor)

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

            В SObjectizer контекстами управляет диспетчер, к какому агента привяжут, так он и будет работать. Нужен агенту эксклюзивный контекст — он привязывается либо к active_obj-диспетчеру, либо к собственному one_thread-диспетчеру. При этом внешний вид актора и его внутреннее поведение никак не меняется.
            В SObjectizer настолько прост, что в нём ненужно разбираться?

            Люди говорят, что проще, чем CAF.


            1. skorokhod
              22.02.2017 21:05

              Не понятно, что подразумевается под статической типизацией. Посмотрите пример из статьи. Метод fixed_stack::on_push будет вызван только для сообщения push, ни для какого другого сообщения его использовать не получится. Возможно это как раз посредством статической типизации.
              Вероятно, вы под статической типизацией понимаете что-то другое. Например, невозможность отправить агенту сообщение типа A, если агент в нем не заинтересован. Если так, то этого в SObjectizer нет. Поскольку на практике с такими задачами не встречались. Где это может потребоваться?

              А что произойдёт с сообщением, для которого не задан обработчик?


              В больших системах это как раз "обыденно", когда сообщение в следствие ошибок заруливается "не туда".
              Правда, бывают ситуации, когда это делается намеренно (для маршрутизаторов), для этого в caf есть default_handler.


              1. eao197
                22.02.2017 21:09

                А что произойдёт с сообщением, для которого не задан обработчик?

                Оно будет просто проигнорировано.
                В больших системах это как раз «обыденно», когда сообщение в следствие ошибок заруливается «не туда».

                Доставка сообщений вообще в принципе ненадежна. Так что заруливание «не туда» или потеря по какой-то другой причине — особой разницы нет.
                Правда, бывают ситуации, когда это делается намеренно (для маршрутизаторов), для этого в caf есть default_handler.

                Не понятно, зачем это нужно. Но, раз сделали, значит нужно зачем-то.


                1. skorokhod
                  22.02.2017 23:51

                  Доставка сообщений вообще в принципе ненадежна. Так что заруливание «не туда» или потеря по какой-то другой причине — особой разницы нет.

                  С таким же успехом можно сказать "вызов функций в принципе ненадёжен"…
                  Надёжность определяется средой передачи и протоколом.


                  Не понятно, зачем это нужно. Но, раз сделали, значит нужно зачем-то.

                  Нужно это примерно для того же, для чего разработчики protobuf создали тип Any.


                  1. eao197
                    23.02.2017 08:29

                    С таким же успехом можно сказать «вызов функций в принципе ненадёжен»…

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

                    В случае отсылки сообщения отправитель продолжает нормально работать пока сообщение дойдет до получателя. Дойдет или нет — никто никаких гарантий не дает. Есть ряд причин, по которым доставка и обработка отосланного сообщения может не состоятся.
                    Надёжность определяется средой передачи и протоколом.

                    Странная фраза. Могли вы пояснить, какое отношение она имеет к обмену сообщениями между акторами внутри одного процесса (хотя бы одного процесса для простоты)?
                    Нужно это примерно для того же, для чего разработчики protobuf создали тип Any.

                    Для чего нужно нечто, что можно охарактеризовать как opaque payload, в транспортных протоколах — понятно. Зачем акторам иметь возможность получить сообщение, типа которого они не знают — нет. Можно прикладной пример?


  1. dendron
    23.02.2017 20:28

    Если не затруднит, можно в двух словах в чём преимущества акторов перед "чистой" моделью передачи сообщений, той же MPI?


    1. eao197
      23.02.2017 21:59
      +1

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

      На мой взгляд, Модель Акторов — это следующий логический шаг в степени использования message-passing. Т.е. сперва вы решаете, что вам нужны независимые потоки управления, у каждого из которых свои собственные данные. И для общения между потоками вам нужен обмен сообщениями.

      Затем вы понимаете, что у ваших потоков появляется какое-то сложное поведение, которое зависит от того, какие сообщения поток получает. Потом вы обнаруживаете, что можно как бы разделить логические потоки и физические. И что логических потоков вам нужно больше, чем физических. Вам теперь нужно отобразить N логических потоков на M физических.

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

      Ну и еще замечу, что MPI предназначен для решения проблем parallel computing, тогда как акторы — для concurrent computing. Специфика немного разная.