C++ Russia 2017
Для чего мы делали свой
акторный фреймворк и что из
этого вышло?
Евгений Охотников
А делали ли мы акторный фреймворк вообще?
?
2
Давным-давно, в далекой-далекой...
1994-й год.
Гомель.
КБ Системного Программирования.
Отдел АСУТП.
Попытка сделать объектно-ориентированную SCADA-систему.
SCADA - Supervisory Control And Data Acquisition
3
О SCADA-системах в двух словах
4
http://www.scadasoftware.net/
Объектная SCADA. Зачем?
Традиционный подход SCADA-систем в те времена:
● набор нумерованных или именованных каналов ввода-вывода (тегов);
● слабая структуризация и почти отсутствие декомпозиции;
● трудозатраты растут с увеличением числа тегов;
● трудозатраты еще быстрее растут по мере усложнения логики
автоматизируемого техпроцесса.
Мы хотели устранить это за счет декомпозиции на объекты,
обладающие состоянием и поведением.
5
Объектная SCADA. Что получилось?
Агенты:
● конечные автоматы с явно выделенными состояниями;
● состояния агентов видны снаружи.
6
Объектная SCADA. Что получилось?
Взаимодействие через асинхронные сообщения:
● прицел на soft real-time;
● распространение информации как в режиме 1:1, так и в
режиме 1:N.
7
Объектная SCADA. Что получилось?
Диспетчер.
Отвечал за обработку очередей сообщений с учетом приоритетов
и требований soft real-time.
8
Объектная SCADA. Итог
SCADA Objectizer.
1998-й год.
Опробован в реальном проекте.
Прекратил свое существование в начале 2000-го :(
9
Предпосылки к перерождению
Начало 2002-го.
Компания "Интервэйл".
Двое участников разработки SCADA Objectizer.
Небольшая GUI-программа для управления подключенными к ПК
устройствами...
10
Четвертое пришествие
Апрель 2002-го года.
SObjectizer = SCADA + Objectizer.
SObjectizer-4 (в четвертый раз все сначала).
11
Этапы большого пути
Май 2002-го: начало использования SO-4 в разработке.
Март 2006-го: перевод SO-4 в категорию OpenSource проектов
под BSD-лицензией.
Сентябрь 2010-го: начало работ над SObjectizer-5.
Май 2013-го: публичный релиз SO-5 под BSD-лицензией.
Июль 2013-го: SObjectizer начал жить независимо от "Интервэйл".
12
На данный момент...
Последняя стабильная версия SObjectizer-5.5.18 – это:
● 25KLOC кода (+28KLOC кода в тестах +10KLOC кода в
примерах);
● работа на платформах Windows, Linux, FreeBSD, MacOS, Android
(через CrystaX NDK);
● поддержка компиляторов VC++ 12.0-14.0, GCC 4.8-6.3, clang
3.5-3.9;
● документация, презентации, статьи;
● отсутствие больших ломающих изменений с октября 2014-го.
13
Тем не менее...
Для чего же мы делали свой
фреймворк?
14
Как перестать бояться и...
...полюбить многопоточность?
Голые thread, mutex и condition_variable – это пот, кровь и боль.
Асинхронный обмен сообщениями рулит!
И бибикает :)
Ибо shared nothing и вот этот вот все.
15
Бонусы асинхронного обмена сообщениями
● у каждого агента свое изолированное состояние (принцип
shared nothing), упрощает жизнь в многопоточном коде;
● обмен сообщениями – естественный подход к решению
некоторых типов задач;
● слабая связность между компонентами;
● очень простая работа с таймерами (отложенные и
периодические сообщения);
● низкий порог входа для новичков.
16
Давайте посмотрим на
SObjectizer-5 с более близкого
расстояния
17
В SObjectizer есть сообщения
Все взаимодействие между агентами в SObjectizer идет только
через асинхронные сообщения.
Сообщения это объекты. Тип сообщения наследуется от
so_5::message_t.
18
Выглядят сообщения вот так:
struct request : public so_5::message_t
{
std::int64_t id_;
std::map<std::string, std::string> params_;
std::vector<std::uint8_t> payload_;
std::chrono::steady_clock::timepoint deadline_;
request(
std::int64_t id,
std::map<std::string, std::string> params,
std::vector<std::uint8_t> payload,
std::chrono::steady_clock::timeout deadline)
: id_(id), params_(std::move(params)), payload_(std::move(payload)), deadline_(deadline)
{}
};
struct get_status : public so_5::signal_t {};
19
Отсылаются сообщения вот так:
// Безотлагательная отсылка сообщения.
so_5::send<request>(target,
// Все это через perfect-forwarding идет в конструктор request-а.
make_id(), make_params(), make_payload(), calculate_deadline());
// Безотлагательная отсылка сигнала.
so_5::send<get_status>(target);
// Отсылка сигнала с задержкой на 250ms.
so_5::send_delayed<get_status>(target, std::chrono::milliseconds(250));
// Периодическая отсылка сигнала со стартовой задержкой в 250ms
// и периодом повтора в 750ms.
auto timer = so_5::send_periodic<get_status>(target,
std::chrono::milliseconds(250),
std::chrono::milliseconds(750));
20
Что такое Target для send?
В "традиционной" Модели Акторов адресатом сообщения
является актор.
В SObjectizer сообщения отсылаются в "почтовый ящик".
Почтовый ящик в SObjectizer называется mbox.
21
Подписка на сообщение из mbox-а
Для получения сообщения из mbox-а нужно выполнить подписку.
Только после этого сообщения будут доставляться агенту.
Подписку можно отменить. Сообщения доставляться перестанут.
Подписки агента автоматически удаляются, когда агент
уничтожается.
Сообщения диспетчируются по типу, а не по содержимому.
22
Подписка на сообщение из mbox-а
23
Multi-Producer/Single-Consumer Mbox
Кто угодно может оправить. Подписаться может только один
агент.
MPSC-mbox создается для каждого агента автоматически.
Использование MPSC-mbox-ов дает максимально близкое
приближение к Модели Акторов.
24
Multi-Producer/Multi-Consumer Mbox
Кто угодно может оправить. Получат все подписчики.
MPMC-mbox нужно создавать вручную (можно с именем, можно
без).
Простейший вариант модели Publish/Subscribe.
25
В SObjectizer есть диспетчеры
Именно диспетчер определяет где и когда агент будет
обрабатывать свои сообщения.
Каждый агент в SObjectizer должен быть привязан к конкретному
диспетчеру.
В приложении может быть запущено сразу несколько
диспетчеров.
26
Примеры диспетчеров
one_thread: все агенты на одной и той же нити, все агенты используют
одну очередь сообщений.
active_obj: у каждого агента своя собственная нить, у каждого агента
собственная очередь сообщений.
adv_thread_pool: агенты на группе нитей, агент может мигрировать с
одной нити на другую, события одного агента могут быть запущены
параллельно, если они отмечены как thread_safe. Очереди сообщений
могут быть персональными или общими для нескольких агентов.
27
Больше диспетчеров, хороших и разных
Всего в SObjectizer-5 "из коробки" доступно восемь типов
диспетчеров:
active_group
active_obj
adv_thread_pool
one_thread
prio_dedicated_threads::one_per_prio
prio_one_thread::quoted_round_robin
prio_one_thread::strictly_ordered
thread_pool
28
Зачем так много?
29
В SObjectizer есть агенты
Как правило, реализуются как объекты классов, унаследованных
от so_5::agent_t.
class hello_world final : public so_5::agent_t {
public :
using so_5::agent_t::agent_t;
virtual void so_evt_start() override {
std::cout << "Hello, World!" << std::endl;
so_deregister_agent_coop_normally();
}
};
30
Агенты – это конечные автоматы
1. Мы не поддерживаем агентов в виде сопрограмм. Поэтому
должен использоваться механизм callback-ов, чтобы
отобразить N агентов на M рабочих нитей.
2. Ноги у SObjectizer растут из мира АСУТП, там конечные
автоматы – это обычное дело.
3. Когда есть ярко выраженные состояния и разное поведение в
каждом из состояний конечный автомат удобнее, чем вызовы
receive с последующим выбором обработчика.
31
Агент blinking_led (диаграмма состояний)
32
Агент blinking_led (код)
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 {};
blinking_led( context_t ctx ) : so_5::agent_t{ std::move(ctx) } {
this >>= off;
off.just_switch_to< turn_on_off >( blinking );
blinking.just_switch_to< turn_on_off >( off );
blink_on
.on_enter( []{ std::cout << "ON" << std::endl; } )
.on_exit( []{ std::cout << "off" << std::endl; } )
.time_limit( std::chrono::milliseconds{1500}, blink_off );
blink_off
.time_limit( std::chrono::milliseconds{750}, blink_on );
}
};
33
Агент blinking_led (пояснения)
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 {};
blinking_led( context_t ctx ) : so_5::agent_t{ std::move(ctx) } {
this >>= off;
off.just_switch_to< turn_on_off >( blinking );
blinking.just_switch_to< turn_on_off >( off );
blink_on
.on_enter( []{ std::cout << "ON" << std::endl; } )
.on_exit( []{ std::cout << "off" << std::endl; } )
.time_limit( std::chrono::milliseconds{1500}, blink_off );
blink_off
.time_limit( std::chrono::milliseconds{750}, blink_on );
}
};
34
Определение двух
верхнеуровневых
состояний.
Агент blinking_led (пояснения)
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 {};
blinking_led( context_t ctx ) : so_5::agent_t{ std::move(ctx) } {
this >>= off;
off.just_switch_to< turn_on_off >( blinking );
blinking.just_switch_to< turn_on_off >( off );
blink_on
.on_enter( []{ std::cout << "ON" << std::endl; } )
.on_exit( []{ std::cout << "off" << std::endl; } )
.time_limit( std::chrono::milliseconds{1500}, blink_off );
blink_off
.time_limit( std::chrono::milliseconds{750}, blink_on );
}
};
35
Определение двух вложенных
состояний для состояния blinking.
Подсостояние blink_on является
начальным подсостоянием.
Агент blinking_led (пояснения)
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 {};
blinking_led( context_t ctx ) : so_5::agent_t{ std::move(ctx) } {
this >>= off;
off.just_switch_to< turn_on_off >( blinking );
blinking.just_switch_to< turn_on_off >( off );
blink_on
.on_enter( []{ std::cout << "ON" << std::endl; } )
.on_exit( []{ std::cout << "off" << std::endl; } )
.time_limit( std::chrono::milliseconds{1500}, blink_off );
blink_off
.time_limit( std::chrono::milliseconds{750}, blink_on );
}
};
36
Перевод агента в то состояние, в
котором он должен начать свою работу.
Кому не нравится перегрузка >>= для смены состояния
агента, для тех есть альтернативный синтаксис:
off.activate();
so_change_state(off);
Агент blinking_led (пояснения)
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 {};
blinking_led( context_t ctx ) : so_5::agent_t{ std::move(ctx) } {
this >>= off;
off.just_switch_to< turn_on_off >( blinking );
blinking.just_switch_to< turn_on_off >( off );
blink_on
.on_enter( []{ std::cout << "ON" << std::endl; } )
.on_exit( []{ std::cout << "off" << std::endl; } )
.time_limit( std::chrono::milliseconds{1500}, blink_off );
blink_off
.time_limit( std::chrono::milliseconds{750}, blink_on );
}
};
37
Реакция на сигнал включения и
выключения для верхних состояний.
Достаточно просто перейти в другое
состояние.
Агент blinking_led (пояснения)
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 {};
blinking_led( context_t ctx ) : so_5::agent_t{ std::move(ctx) } {
this >>= off;
off.just_switch_to< turn_on_off >( blinking );
blinking.just_switch_to< turn_on_off >( off );
blink_on
.on_enter( []{ std::cout << "ON" << std::endl; } )
.on_exit( []{ std::cout << "off" << std::endl; } )
.time_limit( std::chrono::milliseconds{1500}, blink_off );
blink_off
.time_limit( std::chrono::milliseconds{750}, blink_on );
}
};
38
Реакция на вход и
выход из состояния.
Агент blinking_led (пояснения)
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 {};
blinking_led( context_t ctx ) : so_5::agent_t{ std::move(ctx) } {
this >>= off;
off.just_switch_to< turn_on_off >( blinking );
blinking.just_switch_to< turn_on_off >( off );
blink_on
.on_enter( []{ std::cout << "ON" << std::endl; } )
.on_exit( []{ std::cout << "off" << std::endl; } )
.time_limit( std::chrono::milliseconds{1500}, blink_off );
blink_off
.time_limit( std::chrono::milliseconds{750}, blink_on );
}
};
39
time_limit задает ограничение на
время пребывания агента в
состоянии.
По истечении этого времени агент
автоматически меняет состояние.
Еще один пример: request_processor (код)
class request_processor : public so_5::agent_t {
state_t st_waiting{ this }, st_working{ this }, ...;
const so_5::mbox_t src_;
public :
request_processor(context_t ctx, so_5::mbox_t src) : so_5::agent_t(std::move(ctx)), src_(std::move(src)) {}
virtual void so_define_agent() override {
this >>= st_waiting;
st_waiting
.event(src_, &request_processor::on_request)
.event(src_, &request_processor::on_status_when_waiting);
st_working
.event(src_, &request_processor::on_request)
.event(src_, &request_processor::on_status_when_working);
...
}
...
private :
void on_request(const request & cmd) {...}
void on_status_when_waiting(mhood_t<get_status>) {...}
void on_status_when_working(mhood_t<get_status>) {...}
};
40
Еще один пример: request_processor (пояснения)
class request_processor : public so_5::agent_t {
state_t st_waiting{ this }, st_working{ this }, ...;
const so_5::mbox_t src_;
public :
request_processor(context_t ctx, so_5::mbox_t src) : so_5::agent_t(std::move(ctx)), src_(std::move(src)) {}
virtual void so_define_agent() override {
this >>= st_waiting;
st_waiting
.event(src_, &request_processor::on_request)
.event(src_, &request_processor::on_status_when_waiting);
st_working
.event(src_, &request_processor::on_request)
.event(src_, &request_processor::on_status_when_working);
...
}
...
private :
void on_request(const request & cmd) {...}
void on_status_when_waiting(mhood_t<get_status>) {...}
void on_status_when_working(mhood_t<get_status>) {...}
};
41
Почтовый ящик, из которого агент ожидает
запросы. Создается кем-то и отдается
агенту в качестве параметра конструктора.
Еще один пример: request_processor (пояснения)
class request_processor : public so_5::agent_t {
state_t st_waiting{ this }, st_working{ this }, ...;
const so_5::mbox_t src_;
public :
request_processor(context_t ctx, so_5::mbox_t src) : so_5::agent_t(std::move(ctx)), src_(std::move(src)) {}
virtual void so_define_agent() override {
this >>= st_waiting;
st_waiting
.event(src_, &request_processor::on_request)
.event(src_, &request_processor::on_status_when_waiting);
st_working
.event(src_, &request_processor::on_request)
.event(src_, &request_processor::on_status_when_working);
...
}
...
private :
void on_request(const request & cmd) {...}
void on_status_when_waiting(mhood_t<get_status>) {...}
void on_status_when_working(mhood_t<get_status>) {...}
};
42
Метод so_define_agent() дает
возможность агенту произвести
"настройку" перед тем, как начать
работать внутри SObjectizer.
Обычно используется для создания
подписок.
Еще один пример: request_processor (пояснения)
class request_processor : public so_5::agent_t {
state_t st_waiting{ this }, st_working{ this }, ...;
const so_5::mbox_t src_;
public :
request_processor(context_t ctx, so_5::mbox_t src) : so_5::agent_t(std::move(ctx)), src_(std::move(src)) {}
virtual void so_define_agent() override {
this >>= st_waiting;
st_waiting
.event(src_, &request_processor::on_request)
.event(src_, &request_processor::on_status_when_waiting);
st_working
.event(src_, &request_processor::on_request)
.event(src_, &request_processor::on_status_when_working);
...
}
...
private :
void on_request(const request & cmd) {...}
void on_status_when_waiting(mhood_t<get_status>) {...}
void on_status_when_working(mhood_t<get_status>) {...}
};
43
Подписка на сообщения.
Явным образом задается mbox из которого ожидаются
сообщения.
Тип сообщения не указан явно, он выводится
автоматически из типа аргумента обработчика.
Еще один пример: request_processor (пояснения)
class request_processor : public so_5::agent_t {
state_t st_waiting{ this }, st_working{ this }, ...;
const so_5::mbox_t src_;
public :
request_processor(context_t ctx, so_5::mbox_t src) : so_5::agent_t(std::move(ctx)), src_(std::move(src)) {}
virtual void so_define_agent() override {
this >>= st_waiting;
st_waiting
.event(src_, &request_processor::on_request)
.event(src_, &request_processor::on_status_when_waiting);
st_working
.event(src_, &request_processor::on_request)
.event(src_, &request_processor::on_status_when_working);
...
}
...
private :
void on_request(const request & cmd) {...}
void on_status_when_waiting(mhood_t<get_status>) {...}
void on_status_when_working(mhood_t<get_status>) {...}
};
44
Обработчик для сообщений с типом request.
Такой формат обработчика требует, чтобы
request был сообщением, а не сигналом.
Еще один пример: request_processor (пояснения)
class request_processor : public so_5::agent_t {
state_t st_waiting{ this }, st_working{ this }, ...;
const so_5::mbox_t src_;
public :
request_processor(context_t ctx, so_5::mbox_t src) : so_5::agent_t(std::move(ctx)), src_(std::move(src)) {}
virtual void so_define_agent() override {
this >>= st_waiting;
st_waiting
.event(src_, &request_processor::on_request)
.event(src_, &request_processor::on_status_when_waiting);
st_working
.event(src_, &request_processor::on_request)
.event(src_, &request_processor::on_status_when_working);
...
}
...
private :
void on_request(const request & cmd) {...}
void on_status_when_waiting(mhood_t<get_status>) {...}
void on_status_when_working(mhood_t<get_status>) {...}
};
45
Обработчики для сообщений или сигналов
с типом get_status.
Такой формат обработчика позволяет
обрабатывать и сообщения, и сигналы. Что
удобно при написании обобщенного кода в
агентах.
Кооперации агентов
Кооперации агентов решают задачу одномоментной регистрации
в SObjectizer группы взаимосвязанных агентов.
46
Если бы были супервизоры, то...
47
Супервизоров нет, есть кооперации
48
Наполнение и регистрация коопераций
so_5::environment_t & env = ...; // Доступ к SObjectizer.
// Заставляем SObjectizer создать объект кооперации.
auto coop = env.create_coop( "data_acquire_demo" );
// Наполняем кооперацию прикладными агентами. Заодно привязываем агентов к разным диспетчерам для того,
// чтобы агенты работали на разных рабочих нитях.
coop->make_agent_with_binder<device_reader>(
so_5::disp::one_thread::create_private_disp(env, "device")->binder(),
... );
coop->make_agent<data_processor>(...);
coop->make_agent_with_binder<db_writer>(
so_5::disp::one_thread::create_private_disp(env, "db")->binder(), ... );
// Осталось только зарегистрировать кооперацию.
env.register_coop(std::move(coop));
49
(1)
Наполнение и регистрация коопераций
so_5::environment_t & env = ...; // Доступ к SObjectizer.
// Заставляем SObjectizer создать объект кооперации.
auto coop = env.create_coop( "data_acquire_demo" );
// Наполняем кооперацию прикладными агентами. Заодно привязываем агентов к разным диспетчерам для того,
// чтобы агенты работали на разных рабочих нитях.
coop->make_agent_with_binder<device_reader>(
so_5::disp::one_thread::create_private_disp(env, "device")->binder(),
... );
coop->make_agent<data_processor>(...);
coop->make_agent_with_binder<db_writer>(
so_5::disp::one_thread::create_private_disp(env, "db")->binder(), ... );
// Осталось только зарегистрировать кооперацию.
env.register_coop(std::move(coop));
50
(2)
Наполнение и регистрация коопераций
so_5::environment_t & env = ...; // Доступ к SObjectizer.
// Заставляем SObjectizer создать объект кооперации.
auto coop = env.create_coop( "data_acquire_demo" );
// Наполняем кооперацию прикладными агентами. Заодно привязываем агентов к разным диспетчерам для того,
// чтобы агенты работали на разных рабочих нитях.
coop->make_agent_with_binder<device_reader>(
so_5::disp::one_thread::create_private_disp(env, "device")->binder(),
... );
coop->make_agent<data_processor>(...);
coop->make_agent_with_binder<db_writer>(
so_5::disp::one_thread::create_private_disp(env, "db")->binder(), ... );
// Осталось только зарегистрировать кооперацию.
env.register_coop(std::move(coop));
51
(3)
Регистрация кооперации – это просто?
1. Проверка уникальности имени.
2. Проверка родительской кооперации (если есть).
3. Запрос ресурсов у диспетчеров.
4. Вызов so_define_agent() для каждого агента.
5. Окончательная привязка агентов к диспетчерам, инициация
so_evt_start().
52
По традиции: hello_world
#include <so_5/all.hpp>
class hello_world final : public so_5::agent_t {
public :
using so_5::agent_t::agent_t;
virtual void so_evt_start() override {
std::cout << "Hello, World!" << std::endl;
so_deregister_agent_coop_normally();
}
};
int main() {
so_5::launch([](so_5::environment_t & env) {
env.register_agent_as_coop("hello", env.make_agent<hello_world>());
});
return 0;
}
53
Распределенности в SObjectizer-5 нет
Распределенность "из коробки" была в SObjectizer-4. Но:
● для разных задач нужны разные протоколы (одно дело –
передача телеметрии, другое – передача больших BLOB-ов);
● back pressure в асинхронном обмене сообщении сам по себе
непрост. В случае IPC эта проблема усугубляется;
● интероперабельность с другими языками программирования.
Точнее, ее отсутствие.
Поэтому в SObjectizer-5 распределенности нет.
54
Куда же мы идем?
55
Реализация Модели Акторов не самоцель
У нас нет цели сделать из SObjectizer самую лучшую и/или самую
полноценную реализацию Модели Акторов.
Мы говорим "акторный фреймворк" только потому, что:
● так проще объяснять, что это в принципе такое;
● маркетинг.
56
Publish/Subscribe
Поддержка Publish/Subscribe была с самого начала.
Более того, в SObjectizer-5 понятие direct mbox-а появилось не
сразу, изначально были только MPMC-mbox-ы.
57
CSP (Communicating Sequential Processes)
В декабре 2015-го добавлены message chains.
struct ping {}; struct pong {};
auto ch1 = so_5::create_mchain(env);
auto ch2 = so_5::create_mchain(env);
std::thread pinger{ [ch1, ch2]{
while(true) {
so_5::send<ping>(ch2);
so_5::receive(ch1, so_5::infinite_wait, [](pong){});
}
} };
std::thread ponger{ [ch1, ch2]{
while(true) {
so_5::receive(ch2, so_5::infinite_wait, [&ch1](ping) { so_5::send<pong>(ch1); } );
}
} };
58
CSP (Communicating Sequential Processes)
В декабре 2015-го добавлены message chains.
struct ping {}; struct pong {};
auto ch1 = so_5::create_mchain(env);
auto ch2 = so_5::create_mchain(env);
std::thread pinger{ [ch1, ch2]{
while(true) {
so_5::send<ping>(ch2);
so_5::receive(ch1, so_5::infinite_wait, [](pong){});
}
} };
std::thread ponger{ [ch1, ch2]{
while(true) {
so_5::receive(ch2, so_5::infinite_wait, [&ch1](ping) { so_5::send<pong>(ch1); } );
}
} };
59
Поэтому на самом-то деле...
It's all about in-process message
dispatching!
60
Наша же цель в том, чтобы...
...предоставить разработчику небольшой качественный и
стабильный инструмент.
Практичный и настраиваемый под нужды пользователя.
Стабильность и совместимость. За это мы готовы платить.
Например, поддержка Ubuntu 14.04 LTS и тамошнего gcc-4.8 для
нас важнее, чем возможность использовать C++14 в коде
SObjectizer.
61
Кстати!
О современном C++
62
В двух словах
Современный C++ для нас очень важен.
Даже так: если бы не C++11, SObjectizer-5 вряд ли появился бы.
Есть в C++11 вещи, без которых SObjectizer сейчас сложно
представить...
63
Variadic templates и perfect forwarding
Используются в SObjectizer повсеместно:
template<typename MSG, typename... ARGS>
void send(const mbox_t & to, ARGS &&... args)
{
auto m = make_unique<MSG>(std::forward<ARGS>(args)...);
to->deliver_message(std::move(m));
}
class agent_coop_t {
public :
...
template< class AGENT, typename... ARGS >
AGENT * make_agent( ARGS &&... args ) {
auto a = make_unique< AGENT >( environment(), std::forward<ARGS>(args)... );
return this->add_agent( std::move( a ) );
}
};
64
Лямбды (особенно в сочетании с шаблонами)
Ну очень сильно помогают. В том числе и для обеспечения
гарантий безопасности исключений...
void agent_core_t::next_coop_reg_step__update_registered_coop_map(
const coop_ref_t & coop_ref,
coop_t * parent_coop_ptr )
{
m_registered_coop[ coop_ref->query_coop_name() ] = coop_ref;
m_total_agent_count += coop_ref->query_agent_count();
so_5::details::do_with_rollback_on_exception(
[&] {
next_coop_reg_step__parent_child_relation( coop_ref, parent_coop_ptr );
},
[&] {
m_total_agent_count -= coop_ref->query_agent_count();
m_registered_coop.erase( coop_ref->query_coop_name() );
} );
}
65
(1)
(2)
(3)
auto и decltype
Сложно переоценить важность auto в современном C++.
Особенно для вывода типа результата функции.
template< typename MAIN_ACTION, typename ROLLBACK_ACTION >
auto do_with_rollback_on_exception(
MAIN_ACTION main_action,
ROLLBACK_ACTION rollback_action ) -> decltype(main_action())
{
using result_type = decltype(main_action());
using namespace rollback_on_exception_details;
rollbacker_t< ROLLBACK_ACTION > rollbacker{ rollback_action };
return executor< result_type, MAIN_ACTION, ROLLBACK_ACTION >::exec(
main_action, rollbacker );
}
66
Стандартная библиотека C++11
Появление thread, mutex, condition_variable, atomic, unordered_map
и пр. в стандартной библиотеке C++11 позволило нам избавиться
от такой зависимости, как ACE.
Стало гораздо легче.
Правда, пришлось делать свою реализацию таймеров, но это уже
совсем другая история...
67
Краткое резюме по современному C++
C++11/14 – это уже совсем другой C++.
Использовать современный C++ намного удобнее, особенно, если
есть возможность пользоваться C++14.
Инфраструктура вокруг языка продолжает желать много лучшего.
Но мы над этим работаем ;)
68
Спасибо за терпение!
Вопросы?
69
SObjectizer: https://sourceforge.net/p/sobjectizer или https://github.com/eao197/so-5-5
Документация по 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.
70

Для чего мы делали свой акторный фреймворк и что из этого вышло?

  • 1.
    C++ Russia 2017 Длячего мы делали свой акторный фреймворк и что из этого вышло? Евгений Охотников
  • 2.
    А делали лимы акторный фреймворк вообще? ? 2
  • 3.
    Давным-давно, в далекой-далекой... 1994-йгод. Гомель. КБ Системного Программирования. Отдел АСУТП. Попытка сделать объектно-ориентированную SCADA-систему. SCADA - Supervisory Control And Data Acquisition 3
  • 4.
    О SCADA-системах вдвух словах 4 http://www.scadasoftware.net/
  • 5.
    Объектная SCADA. Зачем? Традиционныйподход SCADA-систем в те времена: ● набор нумерованных или именованных каналов ввода-вывода (тегов); ● слабая структуризация и почти отсутствие декомпозиции; ● трудозатраты растут с увеличением числа тегов; ● трудозатраты еще быстрее растут по мере усложнения логики автоматизируемого техпроцесса. Мы хотели устранить это за счет декомпозиции на объекты, обладающие состоянием и поведением. 5
  • 6.
    Объектная SCADA. Чтополучилось? Агенты: ● конечные автоматы с явно выделенными состояниями; ● состояния агентов видны снаружи. 6
  • 7.
    Объектная SCADA. Чтополучилось? Взаимодействие через асинхронные сообщения: ● прицел на soft real-time; ● распространение информации как в режиме 1:1, так и в режиме 1:N. 7
  • 8.
    Объектная SCADA. Чтополучилось? Диспетчер. Отвечал за обработку очередей сообщений с учетом приоритетов и требований soft real-time. 8
  • 9.
    Объектная SCADA. Итог SCADAObjectizer. 1998-й год. Опробован в реальном проекте. Прекратил свое существование в начале 2000-го :( 9
  • 10.
    Предпосылки к перерождению Начало2002-го. Компания "Интервэйл". Двое участников разработки SCADA Objectizer. Небольшая GUI-программа для управления подключенными к ПК устройствами... 10
  • 11.
    Четвертое пришествие Апрель 2002-гогода. SObjectizer = SCADA + Objectizer. SObjectizer-4 (в четвертый раз все сначала). 11
  • 12.
    Этапы большого пути Май2002-го: начало использования SO-4 в разработке. Март 2006-го: перевод SO-4 в категорию OpenSource проектов под BSD-лицензией. Сентябрь 2010-го: начало работ над SObjectizer-5. Май 2013-го: публичный релиз SO-5 под BSD-лицензией. Июль 2013-го: SObjectizer начал жить независимо от "Интервэйл". 12
  • 13.
    На данный момент... Последняястабильная версия SObjectizer-5.5.18 – это: ● 25KLOC кода (+28KLOC кода в тестах +10KLOC кода в примерах); ● работа на платформах Windows, Linux, FreeBSD, MacOS, Android (через CrystaX NDK); ● поддержка компиляторов VC++ 12.0-14.0, GCC 4.8-6.3, clang 3.5-3.9; ● документация, презентации, статьи; ● отсутствие больших ломающих изменений с октября 2014-го. 13
  • 14.
    Тем не менее... Длячего же мы делали свой фреймворк? 14
  • 15.
    Как перестать боятьсяи... ...полюбить многопоточность? Голые thread, mutex и condition_variable – это пот, кровь и боль. Асинхронный обмен сообщениями рулит! И бибикает :) Ибо shared nothing и вот этот вот все. 15
  • 16.
    Бонусы асинхронного обменасообщениями ● у каждого агента свое изолированное состояние (принцип shared nothing), упрощает жизнь в многопоточном коде; ● обмен сообщениями – естественный подход к решению некоторых типов задач; ● слабая связность между компонентами; ● очень простая работа с таймерами (отложенные и периодические сообщения); ● низкий порог входа для новичков. 16
  • 17.
    Давайте посмотрим на SObjectizer-5с более близкого расстояния 17
  • 18.
    В SObjectizer естьсообщения Все взаимодействие между агентами в SObjectizer идет только через асинхронные сообщения. Сообщения это объекты. Тип сообщения наследуется от so_5::message_t. 18
  • 19.
    Выглядят сообщения воттак: struct request : public so_5::message_t { std::int64_t id_; std::map<std::string, std::string> params_; std::vector<std::uint8_t> payload_; std::chrono::steady_clock::timepoint deadline_; request( std::int64_t id, std::map<std::string, std::string> params, std::vector<std::uint8_t> payload, std::chrono::steady_clock::timeout deadline) : id_(id), params_(std::move(params)), payload_(std::move(payload)), deadline_(deadline) {} }; struct get_status : public so_5::signal_t {}; 19
  • 20.
    Отсылаются сообщения воттак: // Безотлагательная отсылка сообщения. so_5::send<request>(target, // Все это через perfect-forwarding идет в конструктор request-а. make_id(), make_params(), make_payload(), calculate_deadline()); // Безотлагательная отсылка сигнала. so_5::send<get_status>(target); // Отсылка сигнала с задержкой на 250ms. so_5::send_delayed<get_status>(target, std::chrono::milliseconds(250)); // Периодическая отсылка сигнала со стартовой задержкой в 250ms // и периодом повтора в 750ms. auto timer = so_5::send_periodic<get_status>(target, std::chrono::milliseconds(250), std::chrono::milliseconds(750)); 20
  • 21.
    Что такое Targetдля send? В "традиционной" Модели Акторов адресатом сообщения является актор. В SObjectizer сообщения отсылаются в "почтовый ящик". Почтовый ящик в SObjectizer называется mbox. 21
  • 22.
    Подписка на сообщениеиз mbox-а Для получения сообщения из mbox-а нужно выполнить подписку. Только после этого сообщения будут доставляться агенту. Подписку можно отменить. Сообщения доставляться перестанут. Подписки агента автоматически удаляются, когда агент уничтожается. Сообщения диспетчируются по типу, а не по содержимому. 22
  • 23.
  • 24.
    Multi-Producer/Single-Consumer Mbox Кто угодноможет оправить. Подписаться может только один агент. MPSC-mbox создается для каждого агента автоматически. Использование MPSC-mbox-ов дает максимально близкое приближение к Модели Акторов. 24
  • 25.
    Multi-Producer/Multi-Consumer Mbox Кто угодноможет оправить. Получат все подписчики. MPMC-mbox нужно создавать вручную (можно с именем, можно без). Простейший вариант модели Publish/Subscribe. 25
  • 26.
    В SObjectizer естьдиспетчеры Именно диспетчер определяет где и когда агент будет обрабатывать свои сообщения. Каждый агент в SObjectizer должен быть привязан к конкретному диспетчеру. В приложении может быть запущено сразу несколько диспетчеров. 26
  • 27.
    Примеры диспетчеров one_thread: всеагенты на одной и той же нити, все агенты используют одну очередь сообщений. active_obj: у каждого агента своя собственная нить, у каждого агента собственная очередь сообщений. adv_thread_pool: агенты на группе нитей, агент может мигрировать с одной нити на другую, события одного агента могут быть запущены параллельно, если они отмечены как thread_safe. Очереди сообщений могут быть персональными или общими для нескольких агентов. 27
  • 28.
    Больше диспетчеров, хорошихи разных Всего в SObjectizer-5 "из коробки" доступно восемь типов диспетчеров: active_group active_obj adv_thread_pool one_thread prio_dedicated_threads::one_per_prio prio_one_thread::quoted_round_robin prio_one_thread::strictly_ordered thread_pool 28
  • 29.
  • 30.
    В SObjectizer естьагенты Как правило, реализуются как объекты классов, унаследованных от so_5::agent_t. class hello_world final : public so_5::agent_t { public : using so_5::agent_t::agent_t; virtual void so_evt_start() override { std::cout << "Hello, World!" << std::endl; so_deregister_agent_coop_normally(); } }; 30
  • 31.
    Агенты – этоконечные автоматы 1. Мы не поддерживаем агентов в виде сопрограмм. Поэтому должен использоваться механизм callback-ов, чтобы отобразить N агентов на M рабочих нитей. 2. Ноги у SObjectizer растут из мира АСУТП, там конечные автоматы – это обычное дело. 3. Когда есть ярко выраженные состояния и разное поведение в каждом из состояний конечный автомат удобнее, чем вызовы receive с последующим выбором обработчика. 31
  • 32.
  • 33.
    Агент blinking_led (код) classblinking_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 {}; blinking_led( context_t ctx ) : so_5::agent_t{ std::move(ctx) } { this >>= off; off.just_switch_to< turn_on_off >( blinking ); blinking.just_switch_to< turn_on_off >( off ); blink_on .on_enter( []{ std::cout << "ON" << std::endl; } ) .on_exit( []{ std::cout << "off" << std::endl; } ) .time_limit( std::chrono::milliseconds{1500}, blink_off ); blink_off .time_limit( std::chrono::milliseconds{750}, blink_on ); } }; 33
  • 34.
    Агент blinking_led (пояснения) classblinking_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 {}; blinking_led( context_t ctx ) : so_5::agent_t{ std::move(ctx) } { this >>= off; off.just_switch_to< turn_on_off >( blinking ); blinking.just_switch_to< turn_on_off >( off ); blink_on .on_enter( []{ std::cout << "ON" << std::endl; } ) .on_exit( []{ std::cout << "off" << std::endl; } ) .time_limit( std::chrono::milliseconds{1500}, blink_off ); blink_off .time_limit( std::chrono::milliseconds{750}, blink_on ); } }; 34 Определение двух верхнеуровневых состояний.
  • 35.
    Агент blinking_led (пояснения) classblinking_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 {}; blinking_led( context_t ctx ) : so_5::agent_t{ std::move(ctx) } { this >>= off; off.just_switch_to< turn_on_off >( blinking ); blinking.just_switch_to< turn_on_off >( off ); blink_on .on_enter( []{ std::cout << "ON" << std::endl; } ) .on_exit( []{ std::cout << "off" << std::endl; } ) .time_limit( std::chrono::milliseconds{1500}, blink_off ); blink_off .time_limit( std::chrono::milliseconds{750}, blink_on ); } }; 35 Определение двух вложенных состояний для состояния blinking. Подсостояние blink_on является начальным подсостоянием.
  • 36.
    Агент blinking_led (пояснения) classblinking_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 {}; blinking_led( context_t ctx ) : so_5::agent_t{ std::move(ctx) } { this >>= off; off.just_switch_to< turn_on_off >( blinking ); blinking.just_switch_to< turn_on_off >( off ); blink_on .on_enter( []{ std::cout << "ON" << std::endl; } ) .on_exit( []{ std::cout << "off" << std::endl; } ) .time_limit( std::chrono::milliseconds{1500}, blink_off ); blink_off .time_limit( std::chrono::milliseconds{750}, blink_on ); } }; 36 Перевод агента в то состояние, в котором он должен начать свою работу. Кому не нравится перегрузка >>= для смены состояния агента, для тех есть альтернативный синтаксис: off.activate(); so_change_state(off);
  • 37.
    Агент blinking_led (пояснения) classblinking_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 {}; blinking_led( context_t ctx ) : so_5::agent_t{ std::move(ctx) } { this >>= off; off.just_switch_to< turn_on_off >( blinking ); blinking.just_switch_to< turn_on_off >( off ); blink_on .on_enter( []{ std::cout << "ON" << std::endl; } ) .on_exit( []{ std::cout << "off" << std::endl; } ) .time_limit( std::chrono::milliseconds{1500}, blink_off ); blink_off .time_limit( std::chrono::milliseconds{750}, blink_on ); } }; 37 Реакция на сигнал включения и выключения для верхних состояний. Достаточно просто перейти в другое состояние.
  • 38.
    Агент blinking_led (пояснения) classblinking_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 {}; blinking_led( context_t ctx ) : so_5::agent_t{ std::move(ctx) } { this >>= off; off.just_switch_to< turn_on_off >( blinking ); blinking.just_switch_to< turn_on_off >( off ); blink_on .on_enter( []{ std::cout << "ON" << std::endl; } ) .on_exit( []{ std::cout << "off" << std::endl; } ) .time_limit( std::chrono::milliseconds{1500}, blink_off ); blink_off .time_limit( std::chrono::milliseconds{750}, blink_on ); } }; 38 Реакция на вход и выход из состояния.
  • 39.
    Агент blinking_led (пояснения) classblinking_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 {}; blinking_led( context_t ctx ) : so_5::agent_t{ std::move(ctx) } { this >>= off; off.just_switch_to< turn_on_off >( blinking ); blinking.just_switch_to< turn_on_off >( off ); blink_on .on_enter( []{ std::cout << "ON" << std::endl; } ) .on_exit( []{ std::cout << "off" << std::endl; } ) .time_limit( std::chrono::milliseconds{1500}, blink_off ); blink_off .time_limit( std::chrono::milliseconds{750}, blink_on ); } }; 39 time_limit задает ограничение на время пребывания агента в состоянии. По истечении этого времени агент автоматически меняет состояние.
  • 40.
    Еще один пример:request_processor (код) class request_processor : public so_5::agent_t { state_t st_waiting{ this }, st_working{ this }, ...; const so_5::mbox_t src_; public : request_processor(context_t ctx, so_5::mbox_t src) : so_5::agent_t(std::move(ctx)), src_(std::move(src)) {} virtual void so_define_agent() override { this >>= st_waiting; st_waiting .event(src_, &request_processor::on_request) .event(src_, &request_processor::on_status_when_waiting); st_working .event(src_, &request_processor::on_request) .event(src_, &request_processor::on_status_when_working); ... } ... private : void on_request(const request & cmd) {...} void on_status_when_waiting(mhood_t<get_status>) {...} void on_status_when_working(mhood_t<get_status>) {...} }; 40
  • 41.
    Еще один пример:request_processor (пояснения) class request_processor : public so_5::agent_t { state_t st_waiting{ this }, st_working{ this }, ...; const so_5::mbox_t src_; public : request_processor(context_t ctx, so_5::mbox_t src) : so_5::agent_t(std::move(ctx)), src_(std::move(src)) {} virtual void so_define_agent() override { this >>= st_waiting; st_waiting .event(src_, &request_processor::on_request) .event(src_, &request_processor::on_status_when_waiting); st_working .event(src_, &request_processor::on_request) .event(src_, &request_processor::on_status_when_working); ... } ... private : void on_request(const request & cmd) {...} void on_status_when_waiting(mhood_t<get_status>) {...} void on_status_when_working(mhood_t<get_status>) {...} }; 41 Почтовый ящик, из которого агент ожидает запросы. Создается кем-то и отдается агенту в качестве параметра конструктора.
  • 42.
    Еще один пример:request_processor (пояснения) class request_processor : public so_5::agent_t { state_t st_waiting{ this }, st_working{ this }, ...; const so_5::mbox_t src_; public : request_processor(context_t ctx, so_5::mbox_t src) : so_5::agent_t(std::move(ctx)), src_(std::move(src)) {} virtual void so_define_agent() override { this >>= st_waiting; st_waiting .event(src_, &request_processor::on_request) .event(src_, &request_processor::on_status_when_waiting); st_working .event(src_, &request_processor::on_request) .event(src_, &request_processor::on_status_when_working); ... } ... private : void on_request(const request & cmd) {...} void on_status_when_waiting(mhood_t<get_status>) {...} void on_status_when_working(mhood_t<get_status>) {...} }; 42 Метод so_define_agent() дает возможность агенту произвести "настройку" перед тем, как начать работать внутри SObjectizer. Обычно используется для создания подписок.
  • 43.
    Еще один пример:request_processor (пояснения) class request_processor : public so_5::agent_t { state_t st_waiting{ this }, st_working{ this }, ...; const so_5::mbox_t src_; public : request_processor(context_t ctx, so_5::mbox_t src) : so_5::agent_t(std::move(ctx)), src_(std::move(src)) {} virtual void so_define_agent() override { this >>= st_waiting; st_waiting .event(src_, &request_processor::on_request) .event(src_, &request_processor::on_status_when_waiting); st_working .event(src_, &request_processor::on_request) .event(src_, &request_processor::on_status_when_working); ... } ... private : void on_request(const request & cmd) {...} void on_status_when_waiting(mhood_t<get_status>) {...} void on_status_when_working(mhood_t<get_status>) {...} }; 43 Подписка на сообщения. Явным образом задается mbox из которого ожидаются сообщения. Тип сообщения не указан явно, он выводится автоматически из типа аргумента обработчика.
  • 44.
    Еще один пример:request_processor (пояснения) class request_processor : public so_5::agent_t { state_t st_waiting{ this }, st_working{ this }, ...; const so_5::mbox_t src_; public : request_processor(context_t ctx, so_5::mbox_t src) : so_5::agent_t(std::move(ctx)), src_(std::move(src)) {} virtual void so_define_agent() override { this >>= st_waiting; st_waiting .event(src_, &request_processor::on_request) .event(src_, &request_processor::on_status_when_waiting); st_working .event(src_, &request_processor::on_request) .event(src_, &request_processor::on_status_when_working); ... } ... private : void on_request(const request & cmd) {...} void on_status_when_waiting(mhood_t<get_status>) {...} void on_status_when_working(mhood_t<get_status>) {...} }; 44 Обработчик для сообщений с типом request. Такой формат обработчика требует, чтобы request был сообщением, а не сигналом.
  • 45.
    Еще один пример:request_processor (пояснения) class request_processor : public so_5::agent_t { state_t st_waiting{ this }, st_working{ this }, ...; const so_5::mbox_t src_; public : request_processor(context_t ctx, so_5::mbox_t src) : so_5::agent_t(std::move(ctx)), src_(std::move(src)) {} virtual void so_define_agent() override { this >>= st_waiting; st_waiting .event(src_, &request_processor::on_request) .event(src_, &request_processor::on_status_when_waiting); st_working .event(src_, &request_processor::on_request) .event(src_, &request_processor::on_status_when_working); ... } ... private : void on_request(const request & cmd) {...} void on_status_when_waiting(mhood_t<get_status>) {...} void on_status_when_working(mhood_t<get_status>) {...} }; 45 Обработчики для сообщений или сигналов с типом get_status. Такой формат обработчика позволяет обрабатывать и сообщения, и сигналы. Что удобно при написании обобщенного кода в агентах.
  • 46.
    Кооперации агентов Кооперации агентоврешают задачу одномоментной регистрации в SObjectizer группы взаимосвязанных агентов. 46
  • 47.
    Если бы былисупервизоры, то... 47
  • 48.
  • 49.
    Наполнение и регистрациякоопераций so_5::environment_t & env = ...; // Доступ к SObjectizer. // Заставляем SObjectizer создать объект кооперации. auto coop = env.create_coop( "data_acquire_demo" ); // Наполняем кооперацию прикладными агентами. Заодно привязываем агентов к разным диспетчерам для того, // чтобы агенты работали на разных рабочих нитях. coop->make_agent_with_binder<device_reader>( so_5::disp::one_thread::create_private_disp(env, "device")->binder(), ... ); coop->make_agent<data_processor>(...); coop->make_agent_with_binder<db_writer>( so_5::disp::one_thread::create_private_disp(env, "db")->binder(), ... ); // Осталось только зарегистрировать кооперацию. env.register_coop(std::move(coop)); 49 (1)
  • 50.
    Наполнение и регистрациякоопераций so_5::environment_t & env = ...; // Доступ к SObjectizer. // Заставляем SObjectizer создать объект кооперации. auto coop = env.create_coop( "data_acquire_demo" ); // Наполняем кооперацию прикладными агентами. Заодно привязываем агентов к разным диспетчерам для того, // чтобы агенты работали на разных рабочих нитях. coop->make_agent_with_binder<device_reader>( so_5::disp::one_thread::create_private_disp(env, "device")->binder(), ... ); coop->make_agent<data_processor>(...); coop->make_agent_with_binder<db_writer>( so_5::disp::one_thread::create_private_disp(env, "db")->binder(), ... ); // Осталось только зарегистрировать кооперацию. env.register_coop(std::move(coop)); 50 (2)
  • 51.
    Наполнение и регистрациякоопераций so_5::environment_t & env = ...; // Доступ к SObjectizer. // Заставляем SObjectizer создать объект кооперации. auto coop = env.create_coop( "data_acquire_demo" ); // Наполняем кооперацию прикладными агентами. Заодно привязываем агентов к разным диспетчерам для того, // чтобы агенты работали на разных рабочих нитях. coop->make_agent_with_binder<device_reader>( so_5::disp::one_thread::create_private_disp(env, "device")->binder(), ... ); coop->make_agent<data_processor>(...); coop->make_agent_with_binder<db_writer>( so_5::disp::one_thread::create_private_disp(env, "db")->binder(), ... ); // Осталось только зарегистрировать кооперацию. env.register_coop(std::move(coop)); 51 (3)
  • 52.
    Регистрация кооперации –это просто? 1. Проверка уникальности имени. 2. Проверка родительской кооперации (если есть). 3. Запрос ресурсов у диспетчеров. 4. Вызов so_define_agent() для каждого агента. 5. Окончательная привязка агентов к диспетчерам, инициация so_evt_start(). 52
  • 53.
    По традиции: hello_world #include<so_5/all.hpp> class hello_world final : public so_5::agent_t { public : using so_5::agent_t::agent_t; virtual void so_evt_start() override { std::cout << "Hello, World!" << std::endl; so_deregister_agent_coop_normally(); } }; int main() { so_5::launch([](so_5::environment_t & env) { env.register_agent_as_coop("hello", env.make_agent<hello_world>()); }); return 0; } 53
  • 54.
    Распределенности в SObjectizer-5нет Распределенность "из коробки" была в SObjectizer-4. Но: ● для разных задач нужны разные протоколы (одно дело – передача телеметрии, другое – передача больших BLOB-ов); ● back pressure в асинхронном обмене сообщении сам по себе непрост. В случае IPC эта проблема усугубляется; ● интероперабельность с другими языками программирования. Точнее, ее отсутствие. Поэтому в SObjectizer-5 распределенности нет. 54
  • 55.
    Куда же мыидем? 55
  • 56.
    Реализация Модели Акторовне самоцель У нас нет цели сделать из SObjectizer самую лучшую и/или самую полноценную реализацию Модели Акторов. Мы говорим "акторный фреймворк" только потому, что: ● так проще объяснять, что это в принципе такое; ● маркетинг. 56
  • 57.
    Publish/Subscribe Поддержка Publish/Subscribe былас самого начала. Более того, в SObjectizer-5 понятие direct mbox-а появилось не сразу, изначально были только MPMC-mbox-ы. 57
  • 58.
    CSP (Communicating SequentialProcesses) В декабре 2015-го добавлены message chains. struct ping {}; struct pong {}; auto ch1 = so_5::create_mchain(env); auto ch2 = so_5::create_mchain(env); std::thread pinger{ [ch1, ch2]{ while(true) { so_5::send<ping>(ch2); so_5::receive(ch1, so_5::infinite_wait, [](pong){}); } } }; std::thread ponger{ [ch1, ch2]{ while(true) { so_5::receive(ch2, so_5::infinite_wait, [&ch1](ping) { so_5::send<pong>(ch1); } ); } } }; 58
  • 59.
    CSP (Communicating SequentialProcesses) В декабре 2015-го добавлены message chains. struct ping {}; struct pong {}; auto ch1 = so_5::create_mchain(env); auto ch2 = so_5::create_mchain(env); std::thread pinger{ [ch1, ch2]{ while(true) { so_5::send<ping>(ch2); so_5::receive(ch1, so_5::infinite_wait, [](pong){}); } } }; std::thread ponger{ [ch1, ch2]{ while(true) { so_5::receive(ch2, so_5::infinite_wait, [&ch1](ping) { so_5::send<pong>(ch1); } ); } } }; 59
  • 60.
    Поэтому на самом-тоделе... It's all about in-process message dispatching! 60
  • 61.
    Наша же цельв том, чтобы... ...предоставить разработчику небольшой качественный и стабильный инструмент. Практичный и настраиваемый под нужды пользователя. Стабильность и совместимость. За это мы готовы платить. Например, поддержка Ubuntu 14.04 LTS и тамошнего gcc-4.8 для нас важнее, чем возможность использовать C++14 в коде SObjectizer. 61
  • 62.
  • 63.
    В двух словах СовременныйC++ для нас очень важен. Даже так: если бы не C++11, SObjectizer-5 вряд ли появился бы. Есть в C++11 вещи, без которых SObjectizer сейчас сложно представить... 63
  • 64.
    Variadic templates иperfect forwarding Используются в SObjectizer повсеместно: template<typename MSG, typename... ARGS> void send(const mbox_t & to, ARGS &&... args) { auto m = make_unique<MSG>(std::forward<ARGS>(args)...); to->deliver_message(std::move(m)); } class agent_coop_t { public : ... template< class AGENT, typename... ARGS > AGENT * make_agent( ARGS &&... args ) { auto a = make_unique< AGENT >( environment(), std::forward<ARGS>(args)... ); return this->add_agent( std::move( a ) ); } }; 64
  • 65.
    Лямбды (особенно всочетании с шаблонами) Ну очень сильно помогают. В том числе и для обеспечения гарантий безопасности исключений... void agent_core_t::next_coop_reg_step__update_registered_coop_map( const coop_ref_t & coop_ref, coop_t * parent_coop_ptr ) { m_registered_coop[ coop_ref->query_coop_name() ] = coop_ref; m_total_agent_count += coop_ref->query_agent_count(); so_5::details::do_with_rollback_on_exception( [&] { next_coop_reg_step__parent_child_relation( coop_ref, parent_coop_ptr ); }, [&] { m_total_agent_count -= coop_ref->query_agent_count(); m_registered_coop.erase( coop_ref->query_coop_name() ); } ); } 65 (1) (2) (3)
  • 66.
    auto и decltype Сложнопереоценить важность auto в современном C++. Особенно для вывода типа результата функции. template< typename MAIN_ACTION, typename ROLLBACK_ACTION > auto do_with_rollback_on_exception( MAIN_ACTION main_action, ROLLBACK_ACTION rollback_action ) -> decltype(main_action()) { using result_type = decltype(main_action()); using namespace rollback_on_exception_details; rollbacker_t< ROLLBACK_ACTION > rollbacker{ rollback_action }; return executor< result_type, MAIN_ACTION, ROLLBACK_ACTION >::exec( main_action, rollbacker ); } 66
  • 67.
    Стандартная библиотека C++11 Появлениеthread, mutex, condition_variable, atomic, unordered_map и пр. в стандартной библиотеке C++11 позволило нам избавиться от такой зависимости, как ACE. Стало гораздо легче. Правда, пришлось делать свою реализацию таймеров, но это уже совсем другая история... 67
  • 68.
    Краткое резюме посовременному C++ C++11/14 – это уже совсем другой C++. Использовать современный C++ намного удобнее, особенно, если есть возможность пользоваться C++14. Инфраструктура вокруг языка продолжает желать много лучшего. Но мы над этим работаем ;) 68
  • 69.
  • 70.
    SObjectizer: https://sourceforge.net/p/sobjectizer илиhttps://github.com/eao197/so-5-5 Документация по 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. 70