1. CoreHard C++ Autumn 2016
Модель Акторов и C++:
что, зачем и как?
Евгений Охотников
2. О чем пойдет речь?
Многопоточность и parallel concurrent computing.
Модель Акторов в двух-трех-четырех-...-двенадцати слайдах.
Сегодняшние иконы "Модели Акторов". Все две штуки.
А как же C++ (пока его еще не закопали)?
NIH*-синдром или что есть готового для C++?
2* NIH - Not Invented Here
3. Многопоточность – это...
инструмент, который активно применяется в двух очень разных областях:
● parallel computing (одновременно выполняются одинаковые операции
над разными наборами однотипных данных);
● concurrent computing (одновременно выполняются совсем разные
операции, возможно, над совершенно разными данными).
Concurrent computing может быть и без многопоточности. Но на базе
многопоточности concurrent computing доставляет больше всего...
3
4. Далее только о многопоточности...
...применительно к concurrent computing.
Именно здесь Модель Акторов оказывается очень удобной.
А для parallel computing используются другие подходы и другие
инструменты...
4
8. Убрать разделяемое состояние
Ничего не разделяем. Ни за что не боремся.
Пусть каждый поток владеет своими собственными данными. И никто кроме
потока-владельца не имеет к этим данным доступа.
Принцип shared nothing* (широко известен в узких кругах).
8* https://en.wikipedia.org/wiki/Shared_nothing_architecture
9. Убрать разделяемое состояние
Но что делать, если потоку X потребовалась какая-то информация, которая
есть у потока Y?
Что делать, если поток Y хочет, чтобы поток Z обновил какие-то данные у
себя?
9
10. Потокам нужно общаться, но как?
Кажется, что есть два способа:
1. Синхронно.
2. Асинхронно.
Но это только кажется...
10
11. На самом-то деле...
...никакой синхронности, только хардкор асинхронное взаимодействие.
Например, на основе отсылки и приема сообщений:
● поток X отсылает сообщение-запрос потоку Y;
● поток Y когда-то получит запрос потока X, обработает запрос и отошлет
потоку X ответ;
● поток Y отсылает сообщение-обновление потоку Z;
● поток Z когда-то получит сообщение-обновление от потока Y и обновит
те данные, которые принадлежат потоку Z.
11
12. Просто же!
Есть потоки. У каждого есть очередь входящих сообщений.
Поток спит, если очередь входящих сообщений пуста.
Поток просыпается, когда для него появляются входящие сообщения.
Если потоку X нужно что-то от потока Y, то X отсылает сообщение в очередь
входящих сообщений потока Y.
Если поток Y хочет ответить потоку X, то Y отсылает сообщение в очередь
входящих сообщений потока X.
12
13. А если сообщения несут копию данных...
...то мы получаем очень приятный бонус: прозрачный переход к
распределенности.
Если поток Y передал в сообщении Msg копию данных, а не ссылку на них, то
уже не суть важно, пойдет ли сообщение Msg в локальную очередь
входящих сообщений потока Z. Или же в очередь исходящих данных для
передачи на другой хост.
Сообщение самодостаточно. Поэтому может быть сериализовано, передано
по сети, десериализовано и обработано.
13
14. Вот и весь фокус :)
Модель акторов именно про это.
Про изолированные потоки управления.
И про их взаимодействие на основе обмена сообщениями.
14
15. Модель Акторов
Появилась в 1973-ем году благодаря работам Карла Хьюитта (Carl Hewitt).
Была затем развита в 1981-ом Уильямом Клингером (William Clinger).
И в 1985-ом Гулом Агха (Gul Agha).
Это если говорить про формализацию Модели Акторов.
Неформально она открывалась и переоткрывалась множество раз.
15
16. Модель Акторов
Отправные точки для желающих погрузиться в формальную теорию Модели
Акторов:
https://en.wikipedia.org/wiki/History_of_the_Actor_model
https://en.wikipedia.org/wiki/Actor_model
https://en.wikipedia.org/wiki/Actor_model_theory
16
17. Модель Акторов. Основные принципы
● актор – это некая сущность, обладающая поведением;
● акторы реагируют на входящие сообщения;
● получив сообщение актор может:
○ отослать некоторое (конечное) количество сообщений другим
акторам;
○ создать некоторое (конечное) количество новых акторов;
○ определить для себя новое поведение для обработки последующих
сообщений.
17
18. Актор – это некоторая сущность
Не более того.
Актором может быть отдельный процесс. Например, Erlang.
Актором может быть отдельный поток (OS thread, "green" thread, fiber, ...).
Например, goroutine в Go может рассматриваться как актор.
Актором может быть объект, которому кто-то выделяет рабочий контекст.
Например, Akka.
18
19. Еще раз: актор – это некоторая сущность
Модель Акторов не требует, чтобы актор был процессом или потоком, или
конечным автоматом, или чем-то еще.
Поэтому существующие и востребованные на практике реализации Модели
Акторов очень сильно отличаются друг от друга.
19
20. Модели Акторов уже более 40 лет
За это время Модель Акторов пережила несколько волн популярности и
забвения*
Сейчас очередная волна популярности. Она началась где-то около 10-12 лет
назад. Основным двигателем интереса стали Erlang, а затем Akka.
20* Why has the actor model not succeeded?
21. Современные иконы: Erlang
Пожалуй, самая известная реализация – язык Erlang*
http://www.erlang.org/
Не только сам язык, но и Erlang VM, и OTP, и вообще все, что понастроили
вокруг.
21* Я не встречал, чтобы Джо Армстронг говорил, что на Erlang повлияла Модель Акторов
22. Современные иконы: Erlang
Зародился в одной из исследовательских лабораторий Ericsson-а в 1986-ом
году благодаря Джо Армстронгу (Joe Armstrong).
В 1995-ом году после закрытия неудачного проекта AXE-N (на C++, кстати)
был выбран в качестве основного языка для проекта AXD.
Результатом стал успешный программно-аппаратный продукт AXD301 в
котором было более миллиона строк на Erlang.
22
23. Современные иконы: Erlang
В конце 1990-х использование Erlang-а для новых проектов в Ericsson Radio
AB было запрещено.
Джо Армстронг покинул Ericsson.
Erlang ушел в OpenSource.
Erlang доказал свою состоятельность вне Ericsson-а. Ericsson одумался.
В 2004-ом году Джо Армстронг возвратился в Ericsson.
23
24. Современные иконы: Erlang
За последние годы Erlang многократно показывал себя с лучшей стороны в
очень серьезных проектах (например, WhatsApp использует Erlang).
Многие компании убедившись в достоинствах Erlang-а задействуют Erlang в
своих разработках.
Из близких нам примеров: Wargaming.
24
26. Современные иконы: Akka
Фреймворк для Java и Scala.
http://akka.io/
История началась в 2006: Филипп Холлер (Philipp Haller) разработал
реализацию Модели Акторов для стандартной библиотеки языка Scala.
В 2008-ом Джонас Бонер (Jonas Bonér) начал делать Erlang-овский OTP для
Scala на базе акторов из Scala-stdlib*. Первая публичная версия Akka вышла
в 2010-ом.
26* http://www.lightbend.com/akka-five-year-anniversary
27. Современные иконы: Akka
Очень популярна в JVM-мире.
Соответственно, находит широкое применение там, где позиции JVM
традиционно сильны: Web, онлайн-сервисы и суровый энтерпрайз...
Яркие примеры: LinkedIn и Twitter.
Стоящие за Akka люди так же причастны к таким современным buzz-word-ам,
как Reactive Manifesto* и Microservices** :)
27
* http://www.reactivemanifesto.org/
** https://en.wikipedia.org/wiki/Microservices
28. Современные иконы: Akka
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
}
28
}
}
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
}
http://alvinalexander.com/scala/scala-akka-actors-ping-pong-simple-example
29. В чем сила, брат?
Что же такое предлагают своим пользователям Erlang и Akka?
Почему они востребованы и используются в очень серьезных проектах?
29
30. Вот в чем:
1. Простота. Отсутствие разделяемых данных и взаимодействие
посредством асинхронных сообщений спасают от ужасов
программирования на мьютексах.
2. Масштабирование. Акторов может быть хоть миллион. Даже самую
мелкую задачку можно поручить отдельному актору. Акторов можно
распределять по нескольким процессам и нескольким нодам, при
асинхронном обмене сообщениями это не суть важно.
3. Отказоустойчивость. Какие-то акторы могут "падать", другие акторы
это обнаружат и исправят (см. систему супервизоров Erlang-а*). Shared
nothing + message-passing сильно помогают и тут.
30* http://erlang.org/doc/man/supervisor.html
31. Так говорил Джо Армстронг
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 сделает
программирование параллельных систем с использованием традиционных мьютексов и
разделяемых структур данных сложным до невозможности, и что именно обмен
сообщениями станет доминирующим способом разработки параллельных систем.
Disclaimer: перевод приблизительный, но основной смысл передан верно.
31A History of Erlang, Joe Armstrong, 2007.
32. Кстати об отказоустойчивости
Erlang и Java/Scala (речь об Akka) – это безопасные языки, работающие в
управляемых средах (Erlang VM и JVM).
На отказоустойчивость это влияет самым непосредственным образом.
Деление на ноль в одном из процессов внутри Erlang VM и деление на ноль в
одном из потоков большого приложения на C++ – это две большие разницы.
32
33. А что с C++?
Есть ли смысл в использовании Модели Акторов в C++?
33
36. Так нужен ли вообще C++?
С++ старый язык (более 30 лет с публичного релиза).
За это время стал настоящим монстром, стандарт которого насчитывает
~1500 страниц. И это еще C++17 не приняли...
Вобрал в себя множество нового (в переводе на русский: стал еще сложнее).
Сохранил при этом совместимость с еще более древним языком С. Так что с
отстрелом конечностей все в порядке.
Каждый большой проект использует собственное подмножество C++.
Огромное количество копролитов мамонта легаси кода.
36
37. Тем не менее...
...какой еще мейнстримовый нативный язык без GC:
● настолько же быстр и эффективен?
● позволяет работать на самом низком уровне?
● позволяет подниматься на высокие уровни абстракции (ООП,
обобщенное программирование, метапрограммирование)?
● снабжен таким же количеством разнообразного инструментария?
● так же хорошо и досконально описан в огромном количестве книг,
статей, руководств, рекомендаций и пр.?
● обладает таким же большим коммьюнити?
Если объективно посмотреть по сторонам, то альтернатив не так уж и много.
37
38. Вывод прост: нужен, однозначно!
А раз нужен, то нужны и инструменты, облегчающие жизнь C++
разработчикам.
В том числе и реализации Модели Акторов для C++.
38
39. Больше фреймворков, хороших и разных!
Более-менее актуальный список инструментов для C++ можно найти здесь:
https://en.wikipedia.org/wiki/Actor_model#Actor_libraries_and_frameworks
Там не все. А часть того, что есть, уже умерло. Но все-таки.
39
40. Четверо смелых для беглого знакомства
Далее быстрый забег "по верхам" для знакомства с четырьмя C++
фреймворками, которые:
● на C++;
● подают признаки жизни (что уже не мало, на самом-то деле);
● кросс-платформенны;
● интересны с той или иной стороны.
Есть еще, например, OOSMOS* и Asyncronous Agents Library** от MS, но они
не вошли в обзор из-за нарушения каких-то из перечисленных выше пунктов.
40
* http://www.oosmos.com/
** https://msdn.microsoft.com/en-us/library/dd492627.aspx
42. QP/C++
Акторы в QP/C++ – это иерархические конечные автоматы. Называются
активными объектами.
Код активных объектов можно писать обычным образом. Т.е. вручную
набирать код C++ классов, методов и вот это вот все.
А можно "нарисовать" активный объект в специальном визуальном
инструменте для проектирования и C++ код будет сгенерирован.
Контекст для работы активных объектов предоставляет QP. В зависимости
от окружения активные объекты могут работать на отдельных нитях. А могут
и сообща на одной-единственной.
42
43. QP/C++ (пример кода: 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
43
44. QP/C++ (пример кода: 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
}
44
46. QP/C++ (пример кода: blinky.cpp, 2)
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);
}
46
47. QP/C++ (пример кода: blinky.cpp, 3)
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;
}
47
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;
}
48. Just::Thread Pro: Actors Edition
http://www.stdthread.co.uk/pro/
C++11.
Платная библиотека.
Автор – Энтони Уильямс (Anthony Williams). Он же написал книгу "C++
Concurrency in Action".
На этом, пожалуй, достоинства заканчиваются :)
Под каждого актора выделяется отдельный поток ОС.
48
50. C++ Actor Framework (он же CAF)
http://www.actor-framework.org/
C++11 и выше (чем выше, тем лучше).
OpenSource под BSD-3-CLAUSE лицензией.
Самая распиаренная реализация Модели Акторов для C++.
Позиционирует себя как очень быстрый фреймворк. Не то, чтобы так уж
заслуженно* ;)
Пока еще не стабилизировался.
50* Performance Comparison SO-5.5.15.2 vs CAF-0.14.4
51. C++ Actor Framework (он же CAF)
Изначально копировал Erlang в C++ настолько близко, насколько это
возможно. Постепенно видоизменяется и приобретает свои уникальные
черты.
Ценой за мимикрию под Erlang являются высокие требования CAF-а к
уровню поддержки стандартов в C++ в компиляторе.
Официальная поддержка Windows/VC++ появилась только после выхода
Visual Studio 2015 update 3.
Есть поддержка распределенности. Собственный протокол с реализацией на
базе Boost::Asio.
51
52. C++ Actor Framework (fixed_stack, 1)
#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")};
}
52
53. C++ Actor Framework (fixed_stack, 2)
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;
}
);
53
54. C++ Actor Framework (fixed_stack, 3)
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;
}
);
54
55. C++ Actor Framework (fixed_stack, 4)
empty_.assign(
[=](push_atom, int what) {
data_.push_back(what);
become(filled_);
},
[=](pop_atom) -> error {
return fixed_stack_errc::pop_from_empty;
}
);
}
55
57. C++ Actor Framework (fixed_stack, 6)
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()
57
58. SObjectizer-5
https://sourceforge.net/projects/sobjectizer/ или https://github.com/eao197/so-5-5
C++11 (минимальные требования к компилятору: GCC 4.8, MSVC++12.0).
OpenSource под BSD-3-CLAUSE лицензией.
С очень долгой историей:
1995-2000: Гомель, КБ Системного Программирования, проект SCADA Objectizer;
2002-...: Гомель-Москва, Интервэйл, проект SObjectizer-4;
2010-...: Гомель-Москва, Интервэйл, The SObjectizer Team, проект SObjectizer-5.
58
59. SObjectizer-5
SO-4 в продакшене c 2002-го года. И до сих пор работает.
SO-5 в продакшене с 2011-го года.
Обратная совместимость – must have.
Без фанатизма, но позволить себе вносить ломающие совместимость
изменения в каждый релиз мы не можем. От слова совсем.
Версия SO-5.5.0 вышла в октябре 2014-го. С тех пор в ветке 5.5.* нет ломающих
изменений. Последняя стабильная версия 5.5.18 – сентябрь 2016.
59
60. SObjectizer-5
Акторы в SO-5 называются агентами. Так сложилось.
Агенты в SO-5 – это иерархические конечные автоматы (вложенные
состояния, обработчики входа-выхода, история, временные лимиты).
Рабочий контекст агентам предоставляют диспетчеры.
Разработчику "из коробки" доступно восемь типов готовых диспетчеров.
Распределенность в SO-5 не поддерживается. Был опыт в SO-4. Поэтому в
SO-5 решили обходиться готовыми сторонними инструментами по задачу
(MQTT, AMQP, HTTP и т.д.).
60
61. SObjectizer-5
Главное отличие от прочих фреймворков: SO-5 – это симбиоз Модели
Акторов, модели Publish/Subscribe. С элементами CSP*
Сообщения отсылаются не агенту, а в message box (mbox). За mbox-ом
может быть один агент, может быть множество агентов. Или ни одного.
Mbox является аналогом Topic-а из Pub/Sub. Отсылка сообщения – аналогом
Publish-а из Pub/Sub. И, как и в Pub/Sub, агент должен подписаться на
сообщение, чтобы получить его.
61* https://en.wikipedia.org/wiki/Communicating_sequential_processes
62. SObjectizer-5 (blinking_led, 1)
#include <iostream>
#include <so_5/all.hpp>
class blinking_led final : public so_5::agent_t
{
state_t off{ this }, blinking{ this },
blink_on{ initial_substate_of{ blinking } },
blink_off{ substate_of{ blinking } };
public :
struct turn_on_off : public so_5::signal_t {};
62
64. SObjectizer-5 (blinking_led, 3)
int main() {
so_5::launch( []( so_5::environment_t & env ) {
so_5::mbox_t m;
env.introduce_coop( [&]( so_5::coop_t & coop ) {
auto led = coop.make_agent< blinking_led >();
m = led->so_direct_mbox();
} );
auto pause = []( unsigned int v ) { std::this_thread::sleep_for( std::chrono::seconds{v} ); };
so_5::send< blinking_led::turn_on_off >( m );
pause( 10 );
so_5::send< blinking_led::turn_on_off >( m );
pause( 5 );
so_5::send< blinking_led::turn_on_off >( m );
pause( 5 );
env.stop();
} );
}
64
65. Заключение 1/3
Модель Акторов – это очень удобный инструмент в случаях, где
использование этой модели уместно1
.
Это уже неоднократно доказывалось успешным применением таких
инструментов, как Erlang и Akka в самых разнообразных проектах.
Да и вообще за асинхронным обменом сообщений между независимыми
сущностями будущее. Прислушайтесь к Джо Армстронгу, он плохого не
посоветует ;)
1)
Не верьте рекламе: уместно не везде.
65
66. Заключение 2/3
Наш опыт показывает, что при наличии подходящих инструментов Модель
Акторов имеет смысл применять и в C++.
При этом для C++ подходящие готовые инструменты уже есть.
На разный вкус и цвет.
И размер кошелька, конечно же.
В коммерческом проекте за QP/C++ и за Just::Thread Pro придется заплатить.
За SObjectizer и CAF – нет. По крайней мере сразу не придется.
66
67. Заключение 3/3
Вот чего делать не стоит, так это браться за написание собственного
акторного фреймворка.
Неблагодарное это дело. Проверено. На людях.
Лучше все-таки взять что-то готовое. Hint: купляйце беларускае.
И пусть кто-нибудь другой весело бегает по граблям многопоточности :)
67
69. Bonus track (SO-5's fixed_stack, 1)
#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 {};
69* https://bitbucket.org/sobjectizerteam/fixed_stack_example
73. Документация по SObjectizer: https://sourceforge.net/p/sobjectizer/wiki/Home/
Серия статей о SObjectizer на русском:
SObjectizer: что это, для чего это и почему это выглядит именно так? От простого к сложному:
Часть I, Часть II, Часть III. Акторы в виде конечных автоматов – это плохо или хорошо?
Проблема перегрузки агентов и средства борьбы с ней. Нежная дружба агентов и
исключений.
Серия презентаций о SObjectizer на английском "Dive into SObjectizer-5.5":
Intro, Agent's States, More About Coops, Exceptions, Timers, Synchonous Interaction, Message
Limits, Dispatchers, Message Chains.
Блог автора доклада: eao197.blogspot.com
О SObjectizer в блоге: eao197.blogspot.com/search/label/SObjectizer
Почта автора доклада: eao197 на gmail тчк com
73