А вот у меня школьный вопрос. Если уже существует серверный интерфейс на callback'ах (reactor), можно ли его на клиенте преобразовать в proactor не затрагивая серверный код и используя либо C++ coroutines, либо boost.coroutines? Речь идёт об однопоточном приложении, что-то типа Windows message loop.
Этот трюк называется control flow inversion. Просто когда событий много и они идут в строго определённом порядке (например handshake сетевого протокола), удобно не разбрасывать код по handler'ам, а писать код подряд, что-то вроде:
await;
do_x_stuff(i);
await;
do_y_stuff(i);
Здравствуйте, reversecode, Вы писали:
R>Здравствуйте, swingus, Вы писали:
R>так вы все правильно нарисовали в примере R>в чем проблема ? корутины вам зачем ? мнимую асинхронность хотите добавить ?
ну так это не proactor, с этого и нужно было начинать
тогда берите корутины
в чем проблема не понимаю .. ?
Здравствуйте, swingus, Вы писали:
S>Этот трюк называется control flow inversion. Просто когда событий много и они идут в строго определённом порядке (например handshake сетевого протокола), удобно не разбрасывать код по handler'ам, а писать код подряд, что-то вроде:
S>
S>Здравствуйте, reversecode, Вы писали:
R>>Здравствуйте, swingus, Вы писали:
R>>так вы все правильно нарисовали в примере R>>в чем проблема ? корутины вам зачем ? мнимую асинхронность хотите добавить ?
Здравствуйте, swingus, Вы писали: S>Ну так я и прошу, покажите минимальный пример. У меня нет реального опыта работы с корутинами, а я бы хотел пояснить пару технических вопросов.
Задавай конкретный вопрос. Код сейчас уже подзабыл, но несколько проектов на корутинах в продакшен отправил.
Вкратце, почти любой асинхронный API, принимающий колбек в качестве параметра вызова, можно корутинами превратить в последовательный api.
Асинхронный кошмар
void onSecondOperation(const Result& result, API& api)
{
// process result
api.thirdOperation([&api](const Result& secondOpResult) { onThirdOperation(secondOpResult, api)});
}
void onFirstOperation(const Result& result, API& api)
{
// process result
api.secondOperation([&api](const Result& secondOpResult) { onSecondOperation(secondOpResult, api)});
}
А хотелось бы вот такого:
Result res1 = api.firstOperation();
Result res2 = api.secondOperation();
Result res3 = api.thirdOperation();
Только вызовы эти хотелось бы видеть не блокирующими — поток желательно иметь один.
Делаем примерно так:
// Очевидно, что эта фукнция должна изначально выполняться в контексте corovoid thinkSynchronously(coroutune& coro, API& api)
{
Result res1; // 1
api.firstOperation([&](const Result& result){ res1 = result; coro.run();}); // 2
yield(coro); // 3
Result res2;
api.secondOperation([&](const Result& result){ res2 = result; coro.run();});
yield(coro);
}
В принципе, проще написать тонкую обертку для асинхронного API, которая спрячет 2 и 3 под капотом и добавит обработку ашыпок
Result callMeSynchronously(coroutine& coro, API& asyncApi, Request& request)
{
Result res;
asyncApi.executeRequest(request, [&](const Result& result){ res = result; coro.run();});
yield(coro);
if (!resultOk(res))
{
throw ApiError(getErrorMessage(res));
}
return res;
}
void thinkSynchronously(coroutune& coro, API& api)
{
Result res1 = callMeSynchronously(coro, api, makeSomeRequest());
Result res2 = callMeSynchronously(coro, api, makeSomeRequest());
}
Как-то так.
Подобная схема хорошо работает в случае, когда у тебя есть отдельный worker thread, который вызывает колбеки. Примерно как устроено в boost::asio.
Здравствуйте, landerhigh, Вы писали:
L>Вкратце, почти любой асинхронный API, принимающий колбек в качестве параметра вызова, можно корутинами превратить в последовательный api.
L>
L>Только вызовы эти хотелось бы видеть не блокирующими — поток желательно иметь один.
L>Делаем примерно так:
L>
L>// Очевидно, что эта фукнция должна изначально выполняться в контексте coro
L>void thinkSynchronously(coroutune& coro, API& api)
L>{
L> Result res1; // 1
L> api.firstOperation([&](const Result& result){ res1 = result; coro.run();}); // 2
L> yield(coro); // 3
L> Result res2;
L> api.secondOperation([&](const Result& result){ res2 = result; coro.run();});
L> yield(coro);
L>}
L>
Верхнее — это то, что частенько называют generator, не правда ли? И, это больше похоже на boost.coroutines. Это push или pull coroutine? Пока непонятно, привяжите к конкретной реализации. Лучше всего было бы сделать компилируемый пример.
L>В принципе, проще написать тонкую обертку для асинхронного API, которая спрячет 2 и 3 под капотом и добавит обработку ашыпок
L>
Это я пока не прорубил, давайте вернёмся к этому позднее.
L>Как-то так. L>Подобная схема хорошо работает в случае, когда у тебя есть отдельный worker thread, который вызывает колбеки. Примерно как устроено в boost::asio.
Это неясно, я считал, что затраты равны переключению контекста.
А вот это уже почти pull coroutine.
S>Пока непонятно, привяжите к конкретной реализации.
One step at a time. Я с этими корутинами последний раз работал, когда boost::coroutine еще не существовало, и был просто boost::context
Но в принципе это привязывается к любой реализации. Вся соль в том, что колбек, передаваемый в асинхронный api, передает управление в корутину. Больше тут ничего интересного не происходит. L>>Как-то так. L>>Подобная схема хорошо работает в случае, когда у тебя есть отдельный worker thread, который вызывает колбеки. Примерно как устроено в boost::asio. S>Это неясно, я считал, что затраты равны переключению контекста.
Затраты и правда равны стоимости сохранения и восттановления контекста. Но я не о том говорю.
В случае, когда используемый API сам управляет своими потоками, и вызывает колбеки из них, возникает гонка между повторным вызовом корутины и yield из неё (*1 и *2 наверху) . А это по меньшей мере not nice. Поэтому применительно к примеру сверху, потоками должен полностью заведовать твой код:
void processCallbacks(coroutine& coro, API& api)
{
Result res;
api.setCallback([&coro](const Result& result) {res = result; coro();});
while (true)
{
yield(coro);
processResult(res);
}
}
API api;
coroutine coro([&api](coroutine& coro) {processCallbacks(coro, api);});
while (true) {
api.runWorkerLoop();
}
это все стекфул корутины, не забываем еще про стеклес корутины
там потоков треидов нет
стеклес даже в какой то студии вроде уже реализовывали
хотя официально в стандарт только в С++20 возможно войдут
так что на деле я бы взял обычную стейт машину накатал
нет смысла так заморачиваться
Здравствуйте, reversecode, Вы писали:
R>это все стекфул корутины, не забываем еще про стеклес корутины R>там потоков треидов нет
Стекфул тоже никакого отношения к тредам не имеют (если в детали реализации и страшные слова вродe TLS не залезать)
R>так что на деле я бы взял обычную стейт машину накатал
Это ортогонально. (Если на секунду забыть о том, что накатать "обычную" стейт машину на плюсах квест тот еще)
Это только в мелких примерах с количеством состояний в районе 2 выгоды от корутин не видно. А на деле часто выходит, что нужна такая логика
void performMultiStepOperation(APT& api)
{
while (api.invoke(alive()) == true)
{
if (api.invoke(someRequest()) == 1)
{
if (api.invoke(anotherRequest()) == 12)
{
if (api.invoke(thirdRequest) == 3)
{
throw oopsException();
}
}
}
else
{
switch (api.ivoke(blah()))
{
// lots of cases
}
}
}
}
И вот это разруливать через колбеки с КА — боль и унижение.
R>нет смысла так заморачиваться
Здравствуйте, swingus, Вы писали:
S>А вот у меня школьный вопрос. Если уже существует серверный интерфейс на callback'ах (reactor), можно ли его на клиенте преобразовать в proactor не затрагивая серверный код и используя либо C++ coroutines, либо boost.coroutines? Речь идёт об однопоточном приложении, что-то типа Windows message loop.
Здравствуйте, uncommon, Вы писали:
U>Можно. Советую посмотреть вот эту работу https://isocpp.org/files/papers/n4045.pdf. На странице 24 даже есть примерная реализация. Но придётся попотеть.
Попотеть?
boost::coroutine уже есть.
А стр. 24 сводится к простой идее "Из корутины запускаем асинхронную операцию, которой подсовываем колбек, который вызывает эту же корутину. И сразу делаем yield."
Здравствуйте, landerhigh, Вы писали:
Спасибо, как раз примерялся промоделировать работу нескольких почти независимых микросхем через сопрограммы на "большом" компе.