Coroutines
От: swingus  
Дата: 18.09.17 12:47
Оценка:
А вот у меня школьный вопрос. Если уже существует серверный интерфейс на callback'ах (reactor), можно ли его на клиенте преобразовать в proactor не затрагивая серверный код и используя либо C++ coroutines, либо boost.coroutines? Речь идёт об однопоточном приложении, что-то типа Windows message loop.


// server

using xxx_signal_t = void (*)(int);
using yyy_signal_t = void (*)(int);

void subscribe_for_event_x(xxx_signal_t sig);
void subscribe_for_event_y(yyy_signal_t sig);

// client

void do_x_stuff(int);
void do_y_stuff(int);

void x_handler(int i) 
{
  do_x_stuff(i);
}

void y_handler(int i) 
{
  do_y_stuff(i);
}

int main()
{
  subscribe_for_event_x(&x_handler);
  subscribe_for_event_y(&y_handler);
}
Re: Coroutines
От: reversecode google
Дата: 18.09.17 14:08
Оценка:
Здравствуйте, swingus, Вы писали:

так вы все правильно нарисовали в примере
в чем проблема ? корутины вам зачем ? мнимую асинхронность хотите добавить ?
Re[2]: Coroutines
От: swingus  
Дата: 18.09.17 14:18
Оценка:
Этот трюк называется control flow inversion. Просто когда событий много и они идут в строго определённом порядке (например handshake сетевого протокола), удобно не разбрасывать код по handler'ам, а писать код подряд, что-то вроде:


 await;
 do_x_stuff(i);
 
 await;
 do_y_stuff(i);


Здравствуйте, reversecode, Вы писали:

R>Здравствуйте, swingus, Вы писали:


R>так вы все правильно нарисовали в примере

R>в чем проблема ? корутины вам зачем ? мнимую асинхронность хотите добавить ?
Re[3]: Coroutines
От: reversecode google
Дата: 18.09.17 14:31
Оценка:
ну так это не proactor, с этого и нужно было начинать
тогда берите корутины
в чем проблема не понимаю .. ?

Здравствуйте, swingus, Вы писали:

S>Этот трюк называется control flow inversion. Просто когда событий много и они идут в строго определённом порядке (например handshake сетевого протокола), удобно не разбрасывать код по handler'ам, а писать код подряд, что-то вроде:


S>

S> await;
S> do_x_stuff(i);
 
S> await;
S> do_y_stuff(i);


S>


S>Здравствуйте, reversecode, Вы писали:


R>>Здравствуйте, swingus, Вы писали:


R>>так вы все правильно нарисовали в примере

R>>в чем проблема ? корутины вам зачем ? мнимую асинхронность хотите добавить ?
Re[4]: Coroutines
От: swingus  
Дата: 18.09.17 14:49
Оценка:
Ну так я и прошу, покажите минимальный пример. У меня нет реального опыта работы с корутинами, а я бы хотел пояснить пару технических вопросов.

Здравствуйте, reversecode, Вы писали:

R>ну так это не proactor, с этого и нужно было начинать

R>тогда берите корутины
R>в чем проблема не понимаю .. ?
Отредактировано 18.09.2017 15:01 swingus . Предыдущая версия .
Re[5]: Coroutines
От: landerhigh Пират  
Дата: 18.09.17 22:00
Оценка: 5 (2)
Здравствуйте, 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();


Только вызовы эти хотелось бы видеть не блокирующими — поток желательно иметь один.

Делаем примерно так:

// Очевидно, что эта фукнция должна изначально выполняться в контексте coro
void 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.
Re[6]: Coroutines
От: swingus  
Дата: 19.09.17 00:17
Оценка:
Приятно, когда отвечают по существу.

Здравствуйте, landerhigh, Вы писали:

L>Вкратце, почти любой асинхронный API, принимающий колбек в качестве параметра вызова, можно корутинами превратить в последовательный api.


L>
L>Result res1 = api.firstOperation();
L>Result res2 = api.secondOperation();
L>Result res3 = api.thirdOperation();
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>Result callMeSynchronously(coroutine& coro, API& asyncApi, Request& request)
L>{
L>    Result res;                                     
L>    asyncApi.executeRequest(request, [&](const Result& result){ res = result; coro.run();});  
L>    yield(coro);
L>    if (!resultOk(res))
L>    {
L>        throw ApiError(getErrorMessage(res));
L>    }
L>    return res;
L>}

L>void thinkSynchronously(coroutune& coro, API& api)
L>{
L>    Result res1 = callMeSynchronously(coro, api, makeSomeRequest());
L>    Result res2 = callMeSynchronously(coro, api, makeSomeRequest());
L>}

L>


Это я пока не прорубил, давайте вернёмся к этому позднее.

L>Как-то так.

L>Подобная схема хорошо работает в случае, когда у тебя есть отдельный worker thread, который вызывает колбеки. Примерно как устроено в boost::asio.

Это неясно, я считал, что затраты равны переключению контекста.
Re[7]: Coroutines
От: landerhigh Пират  
Дата: 19.09.17 06:14
Оценка:
Здравствуйте, swingus, Вы писали:

  Кат
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>>


S>Верхнее — это то, что частенько называют generator, не правда ли? И, это больше похоже на boost.coroutines.


Нет, генератор — это способ создать последовательность. Тут же просто управление control flow.

S>Это push или pull coroutine?


В терминах boost::coroutine это симметричная корутина без синтаксического сахара.

В принципе, генератор тоже можно сделать, если, например, к api подписываемся лишь один раз, а потом просто хотим обрабатывать сообщения:

void processCallbacks(coroutine& coro, API& api)
{
    Result res;
    api.setCallback([&coro](const Result& result) {res = result; coro(); // *1});
    while (true)
    {
        yield(coro);               // *2
        processResult(res);
    }
}


А вот это уже почти 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();
}
Re[8]: Coroutines
От: reversecode google
Дата: 19.09.17 09:58
Оценка:
Здравствуйте, landerhigh, Вы писали:

это все стекфул корутины, не забываем еще про стеклес корутины
там потоков треидов нет
стеклес даже в какой то студии вроде уже реализовывали
хотя официально в стандарт только в С++20 возможно войдут

так что на деле я бы взял обычную стейт машину накатал
нет смысла так заморачиваться
Re[9]: Coroutines
От: landerhigh Пират  
Дата: 19.09.17 10:27
Оценка:
Здравствуйте, 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>нет смысла так заморачиваться


Есть и весьма значительный.
Re[10]: Coroutines
От: reversecode google
Дата: 19.09.17 10:57
Оценка:
Здравствуйте, landerhigh, Вы писали:

под капотом у стейфул может быть что угодно, ок если равняться на буст то там зеленые потоки, стек переключение

под капотом у стеклес тот же конечный автомат, только который будет размазывать компилятор (будет в С++20 от Gor N)

заморачиваться на стекфул для данной задачи как автор намекнул на хендснейк это овер
а к тому же так красиво как на стеклес не получится
Re[11]: Coroutines
От: landerhigh Пират  
Дата: 19.09.17 11:04
Оценка:
Здравствуйте, reversecode, Вы писали:

R>заморачиваться на стекфул для данной задачи как автор намекнул на хендснейк это овер


А в чем "заморочка"? Берешь буст, там работает искаропки.

R>а к тому же так красиво как на стеклес не получится


Но это пока еще новости в будущем времени. Можно, конечно, попробовать стеклесс корутины из старой версии boost::asio или какой-нить CO2
Отредактировано 19.09.2017 11:11 landerhigh . Предыдущая версия .
Re: Coroutines
От: uncommon Ниоткуда  
Дата: 24.09.17 01:53
Оценка:
Здравствуйте, swingus, Вы писали:

S>А вот у меня школьный вопрос. Если уже существует серверный интерфейс на callback'ах (reactor), можно ли его на клиенте преобразовать в proactor не затрагивая серверный код и используя либо C++ coroutines, либо boost.coroutines? Речь идёт об однопоточном приложении, что-то типа Windows message loop.


Можно. Советую посмотреть вот эту работу https://isocpp.org/files/papers/n4045.pdf. На странице 24 даже есть примерная реализация. Но придётся попотеть.
Re[2]: Coroutines
От: landerhigh Пират  
Дата: 28.09.17 12:55
Оценка: -1
Здравствуйте, uncommon, Вы писали:

U>Можно. Советую посмотреть вот эту работу https://isocpp.org/files/papers/n4045.pdf. На странице 24 даже есть примерная реализация. Но придётся попотеть.


Попотеть?
boost::coroutine уже есть.
А стр. 24 сводится к простой идее "Из корутины запускаем асинхронную операцию, которой подсовываем колбек, который вызывает эту же корутину. И сразу делаем yield."
Re[6]: Coroutines
От: 8001 Россия  
Дата: 01.10.17 20:19
Оценка:
Здравствуйте, landerhigh, Вы писали:
Спасибо, как раз примерялся промоделировать работу нескольких почти независимых микросхем через сопрограммы на "большом" компе.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.