Upd. Обсуждаемый ниже материал появился на Хабре в виде статьи: Корутины в продакшене: как переложить рельсы под идущим поездом. И в комментариях к этой стате выяснилось, что примеры написаны на CAF для того, чтобы не показывать на слайдах реальный код. Поэтому часть того, что было написано ниже, вероятно, неактуально. И, не исключена вероятность, что на самом деле в качестве агентного фреймворка использовался SObjectizer. Однако, никаких подтверждений этому у меня нет.
На Хабре появился отчет "У нас есть «плюсы» дома: о чем говорили на митапе YADRO × C++ Russia", в котором опубликованы видео докладов "Make legacy great again: переводим зрелый проект на С++20-корутины" и "С++20-корутины в KvadraOS: как устроены и зачем внедрялись". Первый из этих докладов я посмотрел и, как разработчик акторного фреймворка для С++, имею что сказать.
Из рассказанного и показанного складывается ощущение, что отказ от акторов в пользу stackless короутин был полностью оправдан. И тому есть две причины.
Первая и главная, в том, что акторы, реализованные в виде объектов с коллбэками, плохо подходят для "линейного" кода. Т.е. когда ваш алгоритм выражается простой последовательностью шагов, вроде: сделали запрос к подсистеме №1, дождались ответа, сделали запрос к подсистеме №2, дождались ответа, сделали запрос к подсистеме №3 и т.д., то реализация его как актора превратит код в лапшу. Либо в совсем страшную, если все коллбэки представляются лямбдами, либо чуть в менее страшную, если коллбэки -- это обычные методы обычного объекта. Но все равно будет лапша из-за принципиального ограничения такого подхода -- коллбэк не должен "засыпать" и блокировать тред, на котором работает. Обработчик события должен максимально быстро сделать свое дело и вернуть управление фреймворку, отвечающему за вызов коллбэков.
Поэтому-то код на подобных акторах будет представлять из себя цепочку атомарных действий, не связанных в единое целое. Т.е. вместо простого линейного кода вида:
auto r1 = subsystem_1.ask_something();
auto r2 = subsystem_2.ask_something(r1.some_data);
auto r3 = subsystem_3.ask_something(r2.some_data);
make_response(r3.some_data);
Будет что-то вида (псевдокод, не привязанный к конкретному C++ному фреймворку):
my_actor.on([&](start_signal) {
send<ask_something>(subsystem_1).reply_to(my_actor);
});
my_actor.on(subsystem_1, [&](response & r) {
send<ask_something>(subsystem_1, r.some_data).reply_to(my_actor);
});
my_actor.on(subsystem_2, [&](response & r) {
send<ask_something>(subsystem_2, r.some_data).reply_to(my_actor);
});
my_actor.on(subsystem_3, [&](response & r) {
make_response(r.some_data);
});
Поэтому если в вашем распоряжении C++ный акторный фреймворк по типу CAF или SObjectizer и вам нужно писать, в основном, линейный код, то задействовав акторы вы только усложните себе работу.
Акторы будут себя оправдывать в ситуациях, когда вы ожидаете не одно входящее событие, а несколько разных. Т.е. у вас нет сценария "запросили что-то у подсистемы 1 и ждем ответа", а у вас есть сценарий "инициировали несколько действий и далее ждем одно из трех/четырех/пяти/... событий". Вот тогда вы либо превращаете свой код в лапшу через switch-и:
auto msg = receive_message();
switch(msg.type)
{
case event_1: ...; break;
case event_2: ...; break;
case event_3: ...; break;
}
либо выделяете реакцию на каждое событие в отдельный коллбэк и позволяете фреймворку самому разбираться с тем, что именно пришло и какой именно коллбэк нужно вызвать для обработки.
На продемонстрированных в докладе слайдах как раз было видно, что люди имеют дело с простыми последовательными сценариями, в которых акторы на коллбэках показывают себя хуже всего.
Вторая причина, как мне кажется, связана с самим выбранным акторным фреймворком. Вроде бы в докладе его название явным образом не произносили, но судя по примерам кода это был CAF. А у CAF-а, на мой очень субъективный и весьма пристрастный взгляд, есть большие проблемы и с читаемостью, и еще большие с сопровождаемостью.
Во-первых, потому, что виденный мной код акторов на CAF -- это лямбда в лямбде верхом на лямбде и лямбдой погоняет.
Во-вторых, потому, что в виденных мной примерах кода на CAF сообщения принято передавать в виде ad-hoc туплов. Все эти туплы приходится выписывать и в декларациях обработчиков сообщений, и в операциях отсылки сообщений. Что требует затем ручной правки всех этих мест в коде если тупл потребовалось изменить (например, добавить еще что-то в отсылаемое сообщение).
В SObjectizer-е принят совсем другой подход. Поэтому код на SObjectizer, надеюсь, чуть более прост и удобен в сопровождении.
Тут я не хочу сказать, что SO-5 для подобной задачи подходит лучше, чем CAF. Увы, это не так, а почему не так описано выше при разборе первой причины. Но, тешу себя надеждой, что будь этот код написан на SO-5, то сопровождать его было бы проще, чем код на CAF. В том числе и из-за того, что в SO-5 нет таких жестких связей между акторами, чтобы один держал ссылку на другого и это бы мешало завершению работы приложения.
Отдельный вопрос в том, а акторы в принципе подходят под решение задач, подобных рассмотренной в докладе "Make legacy great again: переводим зрелый проект на С++20-корутины"?
Увы, не могу однозначно ответить на данный вопрос, т.к. сама задача была озвучена только поверхностно. С точки зрения банальной эрудиции: почему бы и нет?
Однако, похоже на то, что было выбрано решение "один актор на запрос". Что при большом количестве одновременных запросов ведет к появлению большого количества одновременно живущих акторов. К сожалению, неоднократно убеждался на собственном опыте, что большое количество одновременно живущих акторов -- это не решение проблемы, это сама по себе проблема.
Поэтому лично я бы при попытке подойти к подобной проблеме с акторами на перевес, попробовал бы сперва продумать схему "один актор на операцию". Т.е. актор для авторизации, один актор для выполнения запросов к БД, один актор для протоколирования и т.д., и т.п.
В таком случае акторов было бы меньше, они были бы сложнее, но их было бы проще контролировать. И здесь акторы как раз бы работали не в простых последовательных сценариях, а в ситуациях, где в каждый момент времени может прийти несколько разных событий.
Так же, возможно, акторы были бы вполне уместны даже в схеме "один актор на запрос", но в случае, если у акторов коллбеки могут безопасно и дешево засыпать на синхронных обращениях к другим акторам, не блокируя при этом механизмы диспетчеризации событий в акторном фреймворке. Например, если каждый актор -- это stackfull короутина (fiber, green thread). Тогда актор запросто может заснуть на синхронной операции, а обслуживающий его тред ОС возьмет в работу какого-то другого актора. Но, во-первых, ни CAF, ни SObjectizer подобные акторы не поддерживают. Во-вторых, в докладе отдельно говорится про проблемы, связанные с fiber-ами.
Теперь пара слов о том, а можно ли получить лучшее из двух миров в одном флаконе, т.е. иметь возможность использовать и акторы, и C++20 stackless короутины.
Главным образом вопрос о том, а можно ли научить акторов вызывать co_await внутри своих коллбэков и при этом не требовать от фреймворка создания сотен тысяч тредов для обслуживания всего этого роскошества.
Да, думаю, что можно.
И я пытаюсь научить SO-5 этому. Но не могу сказать, когда же это заработает на практике.
Тут нужно понимать одну простую вещь: SObjectizer напрямую не приносит нам денег, мы не зарабатываем ни на продаже SO-5, ни на его техподдержке, ни на обучении, ни на кастомизации SO-5 под чьи-то специфические нужды. SO-5 и RESTinio создают нам репутацию, которая приводит клиентов. Но клиенты платят за решение своих проблем в своем коде. И эти проблемы, в 99% случаев, вообще никак не связаны с SO-5.
Поэтому работы над SO-5 ведутся лишь тогда, когда для этого удается выкроить ресурсы. То, что могло бы занять 2-3 месяца нормальной работы над SO-5 в режиме "полный рабочий день", растягивается, буквально, на год, если получается выкраивать по 10-15 часов в неделю, да и то далеко не всегда. Посему поддержка короутин в SO-5, надеюсь, появится. Но не через неделю, и даже не через месяц, а может и не через два. Увы, такова се ля ви.
По текущему статусу добавления С++20 короутин в SO-5 могу сказать следующее: часть подготовительных работ выполнена. Дело уперлось в такое понятие, как event_handler_method_t. Эта штука предназначена для хранения обработчиков сообщений и сейчас она заточена только под синхронные обработчики. А нужно поддержать еще и асинхронные. И вопрос в том как это сделать.
Есть один очевидный вариант. Но он потребует ператрахивания (с) значительной части потрохов SO-5. И, вероятно, немного снизит скорость диспетчеризации сообщений (для реальных прикладных программ это вряд ли будет заметно, но вот на тупых бенчмарках, по которым судят о качестве инструмента, просадка может быть заметна). Однако, больше всего меня смущает трудоемкость.
Возможно, найдется какая-то более интересная и простая в реализации идея. Тогда все пойдет гораздо веселее. Но, увы, пока не нашлась.