Comments 96
Почему не захватить this в capture-list лямбды? Тогда весь код внутри вышел бы чуть проще. Но так получилось, что, видимо, лямбда-корутины в компиляторе пока поддерживаются не полностью, поэтому такой код работать не будет.

Это типичная ошибка работы с корутинами: https://quuxplusone.github.io/blog/2019/07/10/ways-to-get-dangling-references-with-coroutines/#exciting-new-way-to-dangle-a-reference и нет, это не недоделка в компиляторе.

Возможно. Но это конечно очень неинтуитивно. Может, все-таки разрешат сохранять в capture-list в корутинах

Начиная с C++17 можно захватывать *this, это именно захват копии класса, созданной через его конструктор копирования.

А, ну да, тут это не поможет. После первого же co_await обращаться к захваченному *this все равно будет нельзя.

// Фиктивный параметр bool B здесь нужен, так как sfinae не работает не на шаблонных функциях

Может можно обойтись if constexpr?

Честно попытался прочитать вашу статью. Но с самого начала преследовало ощущение, что вокруг какая-то невесть откуда взявшаяся сложность. Причем какая-то искуственная сложность. Которая берется не из предметной области, а откуда-то еще.


Но когда дошел вот до этого примера кода:


    template<
        bool B=true,size_t len=sizeof...(T),std::enable_if_t<len!=1 && len!=0 && B, int>=0
    >

то появилась следующая версия: вы умудряетесь писать сложный код на ровном месте. Поскольку вот это же самое сходу можно записать несколько проще и компактнее:


    template<
        bool B=true, std::enable_if_t<B && 1u < sizeof...(T), int>=0
    >

В итоге статью до конца все-таки досмотрел и остался под впечатлением, что вы описали какой-то излишне сложный способ решения какой-то выдуманной задачи. Причем эта выдуманная задача тоже какая-то излишне сложная.


И вот это вот впечатление излишней сложности оставляет негативное ощущение от всей статьи.

Да, здесь я немного пролетел, как мне было уже неоднократно указано.

Просто этот код я брал из проекта, написанного под C++14, да еще и под разные компиляторы (msvc оказыватеся работает со sfinae немного не так, как gcc). И как-то не подумал, что его можно упростить используя более совершенные возможности.

Этот код занимает не очень большую часть статьи, думал, никто не будет акцентировать на нем столько внимания :(
А про искуственную сложность, не понял, в чем там искуственность. Можете предложить более простой подход хотябы к некоторым аспектам статьи?
А про искуственную сложность, не понял, в чем там искуственность.

Если бы мне самому это было понятно, то я бы это уже бы выразил в конкретных замечаниях. А так ощущение есть, но откуда оно происходит не ясно.


Хотя даже если взять ваш первоначальный пример:


    abActor.getA(ABActor::GetACallback(*this, [this](int a) {
        abActor.getB(ABActor::GetBCallback(*this, [a, this](int b) {
            abActor.saveAB(a - b, a + b, ABActor::SaveABCallback(*this, [this](){
                abActor.getA(ABActor::GetACallback(*this, [this](int a) {
                    abActor.getB(ABActor::GetBCallback(*this, [a, this](int b) {
                        std::cout << "Result " << a << " " << b << std::endl;
                    }));
                }));
            }));
        }));
    }));

то непонятно зачем там все это. Вот обращение к некому ABActor::GetACallback оно зачем? И сама запись getA(ABActor::GetACallback()) она какая-то странная. Как будто вы намеренно выставляете напоказ все кишки своего актора.


По идее, вам нужно сделать запрос 'GetA' к актору A, а когда он на этот запрос ответит, вам нужно среагировать на ответ. Что, по идее, должно записываться как-то так (если уж у вас такая тяга к коллбэкам):


send<GetA>(A).then([](int a) {... /* Обработка ответа от A */ });

Ну или так:


A.send<GetA>().then([](int a) {... /* Обработка ответа от A */ });

Без всяких дополнительных ABActor::GetACallback.

Возможно действительно стоило бы написать например через метод then. Но тогда статься бы называлась «пишем акторный фреймворк». Мне хотелось во-первых, обойтись малой кровью, во-вторых, показать идею работы. Я еще в самом начале написал, что не ставлю целью написать полноценный акторный фреймворк. Так что претензия, что «кишик торчат наружу»… Ну, во-первых, не вполне кишки, а во-вторых, «и что»?
Может быть конечно метод then реализуется несложно, но я просто об этом не подумал. Думал, подход с явным созданием структуры будет более наглядным.
Ну, во-первых, не вполне кишки,

Как раз кишки.


а во-вторых, «и что»?

Как минимум, это одна из составляющих впечатления "искуственная сложность".


Ну и позволю себе дать ссылку на свою статью полуторалетней давности: https://habr.com/ru/post/430672/
Там сравнивается использование нескольких подходов к решению одной и той же задачи. И я не могу отделаться от мысли о том, что то, что вы называете "акторным" подходом на самом деле есть task-based подход. И вот как раз в примитивной реализации task-based подхода этот самый callback-hell наступает очень быстро.

> что вы называете «акторным» подходом на самом деле есть task-based подход
Да, возможно. Что-то среднее между акторным и task подходом. Акторный подход, в котором ответчик всегда обязан отдать коллбэк обратно сендеру.

> Как раз кишки.
Не понимаю, причем здесь кишки? Мне кажется, наоборот удобно, что можно посмотреть на тип и понять, какие параметры требуются коллбэку. Как это понять в случае Вашего then?
И к томуже эта структура реализована конкрентым классом акторов. Он вообще может не реализовывать этот объект, и гонять обычные сообщения. Как раз то, что Вы называете «настоящими акторами». Но вот класс захотел и в качестве доп. параметра решил принять некоторую структуру. Причем здесь кишки?

> И вот как раз в примитивной реализации task-based подхода этот самый callback-hell наступает очень быстро.
А как тогда решить эту же задачу без task-based подхода? Хранить состояние, запоминать «этот ответ от сервера мы уже получили»? Тоже hell получается, но уже не callback
Акторный подход, в котором ответчик всегда обязан отдать коллбэк обратно сендеру.

Это что-то новенькое.


Не понимаю, причем здесь кишки?

При том, что это, как минимум, создает дополнительную сложность для читателя вашей статьи.


Мне кажется

А вот мне кажется, что в вашей статье чуть ли не повсюду присутствует искуственная сложность. И я пытаюсь обосновать эти свои ощущения.


Как раз кишки наружу — это и есть один из источников сложности. Пользователь акторного фремворка (даже слепленного на коленке) не должен погружаться в детали его работы. А у вас пользователь вынужден погружаться в это. А читатель вынужден задавать себе вопрос: а зачем в это погружаться, какой в этом смысл?


удобно, что можно посмотреть на тип и понять, какие параметры требуются коллбэку. Как это понять в случае Вашего then?

Вот у вас сигнатура getA:


void getA(const GetACallback &callback);

Глядя на нее нужно приложить некотрые усилия для того, чтобы понять, что callback здесь — это не параметр для выполнения самой операции getA, а нечто, что целевой актор должен вызвать для того, чтобы отдать кому-то результат выполнения операции getA.


Хотя куда естественней было бы видеть интерфейс вида:


int getA();

Тогда видно, что операция getA не требует для своей работы никаких входных параметров и возвращает она int.


А в метод then должен был бы передаваться функтор, который ждет на вход int.


И все оказывается достаточно прозрачно.


Хранить состояние, запоминать «этот ответ от сервера мы уже получили»? Тоже hell получается, но уже не callback

Так вы в итоге и пришли к тому же самому "этот ответ мы уже получили". Просто записали это в виде линейного кода:


const int a = co_await actor.abActor.getAAsync();
const int b = co_await actor.abActor.getBAsync();

Могли бы приблизительно это же самое и на CSP-ных каналах получить.

> а зачем в это погружаться, какой в этом смысл?
Потому что на деталях этого строятся корутины. Я потому и написал свой акторный фреймворк с нуля, чтобы читатель видел, как оно все работает. Без понимания одного не понять другого.
Читатель все равно должен держать в голове всю структуру фреймворка, event loop, треды и т.д. Ну по крайней мере я должен, я иначе не пойму. Кто вызовет корутину, когда результат выполнения «станет доступным»? Исполнится ли корутина в том же треде, в котором была создана? Если Вы сможете это объяснить лучше чем я, то welkome как говорится, с удовольствием прочту Вашу статью. Собственно и написал я эту статью от безысходности, так как ничего подобного на хабре не было (есть нечто подобное в виде видеолекций, но все же).
Это мое понимание того, как должно выглядеть объяснение материала. Если у Вас другое понимание, то спорить не буду, попробуйте объяснить по другому.

> Вот у вас сигнатура getA:
Ну этож коллбэк. Если читатель не знает, что такое коллбэк, ну чтож… Вот берем boost::asio… Ой, смотрите, и там коллбэки.

Можно ли это переписать с помощью then? Может быть и можно, а смысл? Еще раз говорю, что у меня не было цели написать свой акторный фреймворк.

> Могли бы приблизительно это же самое и на CSP-ных каналах получить.
Дак мы акторы или CSP-ные каналы обсуждаем?

Давайте я вам еще раз объясню свою точку зрения:


  • вашу статью тяжело читать потому, что практически везде по тексту возникает ощущение, что автор не может сделать просто. Ну вот тупо не может сделать просто и делает все сложно;
  • вы спросили что именно сложно и я попытался вам донести некоторые соображения;
  • вы эти соображения воспринимаете не как попытки объяснить откуда берется ощущение чрезмерной сложности, а как попытки, ну не знаю, объяснить вам "как нужно было делать";
  • тогда как у меня нет цели рассказать в чем вы не правы.

Соответственно, если вы не желаете (не можете) воспринимать написанное мной именно как попытку объяснить происхождение ощущения искусственной сложности, то вряд ли я еще чем-то могу помочь.


Ну и да. У вас и не акторный подход, и не CSP. Скорее криво написанный и непонятный сходу task-based подход, который вы не смогли простым образом переложить на короутины из C++20. Отсюда и негативное (лично у меня) впечатление от статьи. И именно: автор в принципе не может сделать просто, поэтому делает чрезмерно сложно.

Я бы желал воспринять написанное Вами, но не могу пока найти, что воспринять. Некрасивый sfinae? Ок, мы это уже прошли, к томуже некоторые комментаторы предложили решение получше Вашего. Введение then? А зачем это нужно? Только для того, чтобы ввести?

То что Вы видите, это мой подход к решению подобных задач. Может, он не совсем правильно «акторный», но тем не менее это такой подход и он мне даже нравится. Я по крайней мере не вижу другого способа решить такую задачу. Точнее, способ есть — это fibers с futur-ами, можно попробовать переложить такую задачу на нее, но не факт, что это вообще получится, и если получится, то проще.

Ноги у такой задачи растут много откуда. То же сетевое взаимодействие (boost.asio, libev) основано на примерно таком же подходе. Можно было бы показать на этих примерах, но я решил абстрагироваться от конкретного сетевого фреймворка и написать велосипед.

Как понимаю, основная претензия ко мне в том, что я неаккуратно назвал это «актором». Ну, Вы, как разработчик акторного фреймворка конечно лучше знаете, но может уже стоит перестать цепляться к словам?
Некрасивый sfinae?

Это sfinae является демонстрацией того, что вы не можете сделать просто, поэтому делаете сложно на ровном месте. Это не претензия. И уж тем более не попытка показать, как сделать лучше.


Как понимаю, основная претензия ко мне в том, что я неаккуратно назвал это «актором».

Нет, не понимаете. Вы сделали что-то чрезмено сложное для подобной статьи. Из-за чего статья ваша практически по всей ее протяженности воспринимается с одним и тем же вопросом "ну почему так сложно-то?"


А то, что вы понятия к месту и не к месту путаете, так это, наверное, всего лишь следствие.


То же сетевое взаимодействие (boost.asio, libev) основано на примерно таком же подходе. Можно было бы показать на этих примерах

Ну так и показали бы. Хотя бы на примере работы с тем же Boost.Asio. Типа на Asio с коллбэками мы делаем вот так, а на C++20 короутинах будет вот так. Можно было бы хотя бы о чего-нибудь предметного оттолкнуться. А не от вымышленных бесполезных примеров с непонятным смыслом.

> Это sfinae является демонстрацией того, что вы не можете сделать просто, поэтому делаете сложно на ровном месте.
Ну нашли у меня ошибку. Поправили. Спасибо. Что дальше то ее раскапывать? Если есть что еще сказать, то сказали бы.

> Ну так и показали бы. Хотя бы на примере работы с тем же Boost.Asio
Я решил исповедовать «абстрактный» подход. Думал, что программисты поднаторели в абстракциях, и смогут перенести свою ситуацию на мой код. Но видимо Вам подавай только конкретику. Шаг влево, шаг вправо — расстрел

А про искуственную сложность, не понял, в чем там искуственность.

Ok, в результате спора в комментариях я лучше понял что именно меня смущает в вашей статье. Попробую эти соображения сформулировать.


Первое. Необходимость ручного вызова GetACallback/GetBCallback/etc. Дело в том, что когда речь заходит об акторах, то для акторов thread-safety должна обеспечиваться автоматически самим фреймворкам. Т.е. когда актор A что-то осылает актору B, то актор B гарантированно будет обрабатывать входящее сообщение на своем (и только своем контексте). Тоже самое происходит и с ответом от B к A: этот ответ так же гарантировано будет обрабатываться строго на контексте A. И для обспечения этих свойств пользователю акторного фреймворка делать ничего не нужно.


У вас же в примере thread-safe полуавтоматически обеспечивается только от A к B. Тогда как для ответного сообщения от B к A требуется ручная работа. Именно поэтому программисту вручную приходится вызывать GetACallback при вызове getA.


Как по мне, было бы лучше, если бы getA в таком случае имел бы вид:


A.getA(this, [this](auto a) {...});

вместо:


A.getA(GetACallback(this, [this](auto a){...}));

Именно вот эта потребность в ручном вызове GetACallback и выглядит для меня как выставление "кишок" наружу. Хотя от этих кишок можно было бы легко избавится делая вызов GetACallback прямо внутри getA.


Второе. Мне не хватило раскрытия того, что именно вы подразумеваете под асинхронностью.


Давайте представим, что есть некая сущность S, предоставляющая тот или иной сервис. И есть сущность C, которая пользуется этим сервисом.


Сущность C хочет воспользоваться услугами S в асинхронном режиме. Т.е. сделать что-то типа:


void C::do_something() {
   async(some_context, [&S]{ S.get_service(); });
   ... // Продолжаем работу пока где-то еще S выполняет вызов get_service.
}

Здесь C асинхронно обращается к S.


Но вот собственно работа S::get_service может быть как синхронной (например, get_service выполняет какую-то CPU-bound активность типа шифрования данных или проверки подписи, перекодирования изображений и т.д.), так и асинхронной (например, get_service вызывает неблокирующую операцию записи данных в сокет, которая непонятно когда закончится).


И если S::get_service — это синхронная операция, то возврат ее результата через лямбду — это как-то странно. Можно, конечно, но странно. Тут бы что-то вроде future бы подошло. И код бы имел вид:


void C::do_something() {
   auto f = async(some_context, [&S]{ return S.get_service(); });
   ... // Продолжаем работу пока где-то еще S выполняет вызов get_service.
   f.get();
}

Ну или запись с then была бы вполне уместной: S.ask<get_service>().then(this, [](auto result) {...}).


А вот если вы говорите о случаях, когда сама S::get_service — это асинхронная операция, то тогда вызов callback-а вопросов не вызывает.


Так что если бы вы сделали упор на то, что в первую очередь важна асинхронность самого S::get_service, а не асинхронность обращения от C к S, то мне лично ваш замысел был бы лучше понятен. А так непонятно, на какую именно асинхронность нужно обращать внимание: на то, что C асинхронно обращается к S или на то, что S выполняет свою работу на собственном контексте исполнения только асинхронно.


Третье. Очень нехватает какой-то осмысленной работы внутри актора, который требует у abActor выполнения операций getA, getB и т.д. Т.е. если бы вы сами операции обозвали бы как-то предметно, типа receiveMessage, decryptMessage, storeMessage, да еще и эти операции выполнялись бы разными акторами, да еще если бы результаты этих операций хоть как-то бы анализировались бы… Типа:


mqClient.receiveMessage(this, [this](auto msg) {
   log_incoming(msg);
   if(encryptedMessage(msg)) {
     ++encrypted_stats;
     crypto.decryptMessage(msg.payload(), this, [this, &msg](const auto decrypted) {
       log_decrypted(decrypted);
       msg.setPayload(decrypted);
     });
   }
   db.storeMessage(msg, this, [this](auto result) {...})
});

Ну или что-то в этом духе.


То ваш пример воспринимался бы лучше, т.к. было бы понятно и сколько акторов участвует, и почему важно callback-и вызвать на контексте инициатора асинхронной операции.

Но вот собственно работа S::get_service может быть как синхронной (например, get_service выполняет какую-то CPU-bound активность типа шифрования данных или проверки подписи, перекодирования изображений и т.д.), так и асинхронной (например, get_service вызывает неблокирующую операцию записи данных в сокет, которая непонятно когда закончится).

Ну, этот момент как раз понятен же. Если S — это не только сервис, но ещё и актор, то любую операцию, хоть синхронную, хоть асинхронную, он может выполнять только в собственном потоке. Для остальных потоков она будет выглядеть асинхронной.


И если S::get_service — это синхронная операция, то возврат ее результата через лямбду — это как-то странно. Можно, конечно, но странно. Тут бы что-то вроде future бы подошло.
А вот если вы говорите о случаях, когда сама S::get_service — это асинхронная операция, то тогда вызов callback-а вопросов не вызывает.

А вот это как раз очень странные утверждения. Хотя бы потому что


  1. future замечательно подходит в обоих случаях;
  2. future + then вместе дают точно такой же callback, просто чуть более кружным путём, т.е. задачи "избавиться от колбеков совсем" future не решает.
Если S — это не только сервис, но ещё и актор, то любую операцию, хоть синхронную, хоть асинхронную, он может выполнять только в собственном потоке.

Акцент на то, может ли S::get_service выдать результат на рабочем потоке актора S сразу (т.е. синхронно) или нет. Если может, то у S::get_service может быть сигнатура вида result_type get_service().


Если же get_service и на контексте S должна работать асинхронно (т.е. возврат из get_service произошел, а результат еще неизвестен), то сигнатура S::get_service будет какой-то такой: void get_service(result_handling_callback).


Грубо говоря, можем ли мы тупо написать:


auto f = async(context, [&]{ return S.get_service(); });

или не можем.


future замечательно подходит в обоих случаях;

Я не думаю, что future хорошее решение для случая, когда S::get_service на контексте S асинхронный и имеет сигнатуру вида void get_service(result_handling_callback).


future + then вместе дают точно такой же callback, просто чуть более кружным путём, т.е. задачи "избавиться от колбеков совсем" future не решает.

Речь не про решение, а про восприятие текста статьи и примеров в ней. Если S::get_service исполняется на контексте S синхронно, тогда future.then вполне себе хорошее решение и может рассматриваться вместо наколеночного фреймворка описанного в статье. Если же S::get_service на контексте S асинхронно, то тогда сразу понятно для чего автор начал велосипедить свой фреймворк.

Я не думаю, что future хорошее решение для случая, когда S::get_service на контексте S асинхронный и имеет сигнатуру вида void get_service(result_handling_callback).

Разумеется, сигнатура должна быть вида future<void> get_service(). А потребителю должно быть вообще без разницы, синхронная там на стороне сервиса операция или асинхронная.

сигнатура должна быть вида future<void> get_service()

Скорее future<some_result> get_service().


А потребителю должно быть вообще без разницы, синхронная там на стороне сервиса операция или асинхронная.

А вот при чтении статьи мне лично не понятно, за какую сторону "болеть": за C, который просто хочет вызвать асинхронно S::get_service, за реализацию S::get_service, за обе стороны сразу.

Этот пример показывает, что код на корутины можно переписать частично. Не обязательно перерефакторивать весь проект, чтобы заюзать возможность использовать корутины. Если нам нужна в какойто функции корутина, мы пишем обертки и используем.

Другой вопрос в том, как много этих оберток нужно написать. Сами классы для работы корутин — CoroTask, Awaiter, Resumable пишутся один раз и их можно использовать везде. Но для того, чтобы иметь возможность вызывать «акторный» метод из существующего «актора», для каждого такого метода придется писать доп. обертку. Насколько это применимо? Насколько это удобно в конкретном коде? Это решать уже читателям статьи
Спасибо. Вот это — примерно то, что я ожидал от Вас в первом ну или хотябы во втором сообщении. Давайте теперь более предметно разберем

1) Про GetACallback. В принципе понятно. Я тоже, когда работал над этим примером, думал, стоит ли тащить эту обертку над коллбэком или сделать предложенным Вами методом. Склонился к обертке. Склонился потому, что она обеспечивает более «надежный», хоть и более многословный код. Я правда думал, стоит привносить эту лишнюю надежность в обучающую статью или не стоит, и решил, что хуже не будет. Заодно отсечет лишние вопросы, которые лично я бы автору статьи в таком случае задал.

Ну тоесть ответ такой, что эта обертка просто переносит контроль надежности кода из одного места в другое. Обидно, что из-за этого куска кода развернулась такая полемика. Мне казалось, что я наоборот «изобрел» что-то такое, что можно было бы взять на вооружение.

2) Вот как раз в моем понимании акторы — это всегда асинхронность. Сейчас мы конечно опять начнем полемику о том, что представляют из себя акторы, но я всеже рискну.
Можно ли было сделать этот метод синхронным? Да, но тогда его можно было бы вызвать из разных потоков. Но в этом случае, нам пришлось бы оборачивать среду этого метода в примитивы синхронизации.
Лично я считаю, что программист должен выбирать между акторным подходом, но без мьютексов, или мьютексы, но не-акторный подход. Иначе получается так, что мы огребаем все недостатки как мьютексов, так и акторов, не приобретая никаких достоинств.

Конечно, в реальности все может быть намного сложнее, и в какойто момент для предотврощения распростронения callback hell может оказаться необходимо переделать метод с асинхронного на синхронный, привнеся в код ненавистные мьютексы. Но это обсуждение точно не для обучающей статьи.

3) Про future. Да, такой вариант есть. Но есть как минимум 2 момента — во-первых, коллбэки никуда не пропадают, во-вторых, нужен метод future.then. Но у стандартных future нету метода then. Вроде бы его обещали преподнести как раз в C++20 (но сейчас я его там уже чтото не вижу), но зачем, если у нас и так там уже появятся корутины?

4) Про осмысленный пример. Ок, принято. Я конечно не считаю, что пример должен быть настолько осмысленным, как Вы привели, но признаю, что нормальной осмысленности мне всеже не хватило.
2) Вот как раз в моем понимании акторы — это всегда асинхронность. Сейчас мы конечно опять начнем полемику о том, что представляют из себя акторы, но я всеже рискну.

Акторы — это асинхронность. Но на акторах традиционно это решается по-другому. Как-то так:


void C::do_something() {
   send<get_service>(S, this);
   ...
   receive([this](service_result result) {...});
}
...
void S::get_service(Actor & reply_to) {
   ...
   send<service_result>(reply_to, ...);
}

Так что если бы у вас были традиционные акторы, то вопросов по поводу асинхронности не было бы, т.к. там асинхронность выглядит иначе.


А вот с вашими callback-ами именно что пришлось разбираться с тем, на какую именно асинхронность нужно обращать внимание.

3) Про future. Да, такой вариант есть. Но есть как минимум 2 момента — во-первых, коллбэки никуда не пропадают, во-вторых, нужен метод future.then. Но у стандартных future нету метода then.

Для статьи вместо наколеночного акторного фреймворка вы могли бы навелосипедить эти самые продвинутые future с then. И, поскольку эта концепция достаточно хорошо известна, то исходные примеры с future.then могли бы оказаться более показательными. Т.к. callback hell все равно бы присутствовал. Но хотя бы было сходу понятно что происходит. А так приходится глубоко погружаться в особенности вашего фреймворка.

Это мы уже когдато обсуждали. Писать future.then для того, чтобы написать future.then для этой статьи мне кажется слишком. Одна из целей почти любой статьи «напугать» читателя неправильным подходом, и показать красивый «правильный» подход. Добавление future еще больше напугать читателя не смогло бы. И помочь решить проблему тоже. И поэтому смысла вводить я не вижу.

В чем смысл вводить future, чтобы тутже от него отказаться?
Если хочется иметь красивый линейный код в варианте с коллбэками, то его несложно устроить и без всяких future.
В чем смысл вводить future, чтобы тутже от него отказаться?

Чтобы показать, что короутины делают код еще более прямолинейным и обозримым.


Если хочется иметь красивый линейный код в варианте с коллбэками, то его несложно устроить и без всяких future.

Интересно было бы посмотреть.


Ну и если это так "несложно", то зачем тогда браться за короутины?

auto print = [](int a, int b) {
    std::cout << a << b;
};
auto receivedNewB = [print](int a) {
    abActor.getB(std::bind(print, a, _1));
};
auto receivenNewA = [receivedNewB]() {
    abActor.getA(receivedNewB);
};
auto saveAB = [receivenNewA](int a, int b) {
    abActor.saveAB(a, b, receiveNewA);
};
...


Чуть подредактировал, вроде лучше стало

Тут есть два "но":


  1. Вложенность никуда не ушла. Вы лишь сделали запись лаконичнее. Но если в ней придется разобраться, то все равно придется покурить бамбук на тему того, что и откуда вызывается.


  2. Здесь же как раз нет заботы о том, на каком контексте будут вызываться коллбэки для обработки результатов. А эта забота и была одной из сложностей в статье.


Простые вопросы — а зачем вообще нужна такая адская вложенность callback-ов? Зачем их всех приостанавливать и запускать в произвольном порядке? Что это всё даёт на практике? К чему вся эта явная сложность? Что она упрощает? Может она только усложняет?

Скажем в однопоточном браузере нужны средства для работы с сервером, а поскольку браузер однопоточный, вешать всё на время ожидания ответа сервера негуманно. В браузере можно было бы всё реализовать на callback-ах, но это иногда выглядит плохо читаемо, поэтому там логично видеть await и тому подобное. Но зачем такой же подход тянуть в многопоточный C++? Ну и результирующая сложность реализации такого подхода очевидна из статьи.
Есть «акторный» подход к решению задач. Это один из подходов, не утверждается, что он единственно верный. Но он есть. И зачастую код с акторным подходом превращается в такую «мешанину» коллбэков.

На самом деле, эту «мешанину» коллбэков тоже можно линеаризовать, не прибегая к помощи корутин, но можно поступить и более радикальным способом.

Кроме «акторного» подхода, можно попробовать использовать другие подходы к решению задачи. Вообще, лично я считаю (но это мнение ничем не подкреплено, лишь диванная аналитика), что писать подобный код на корутинах изначально — контрпродуктивно. Лучше попробовать например boost fibers. Но от уже существующего кода избавиться сложнее. Но можно, написав не очень простые обертки, линеаризовать небольшую часть уже существующего кода или писать новые части с использованием такого подхода.
Есть «акторный» подход к решению задач. Это один из подходов, не утверждается, что он единственно верный. Но он есть. И зачастую код с акторным подходом превращается в такую «мешанину» коллбэков.

Акторный подход как бы базируется на отсылке сообщений акторам. А у вас он именно что "акторный", т.к. вместо взаимодействия на сообщениях вы вручную диспетчируете вызовы конкретных коллбэков (с ручным же провязыванием этих коллбэков в цепочки).

Может попробовать выбрать пример, в котором проявляется простота именно после перехода к «акторному» варианту? Даже на статью может потянуть.

Я, имея немалый опыт, почти не сталкивался с подобными примерами. А если сталкивался, то почему-то они в памяти не осели. Обычно всё решается другими методами.
Ну если под «акторным» вариантом понимать мой вариант (не все согласны, что мой вариант настоящий акторный), то я бы предложил любой асинхронный код. Который требует коллбэки.
Мне кажется, если вы сталкивались с асинхронным кодом, то примерно понимаете о чем речь. А если сталкивались и не понимаете, то это уже мне интересно, как вы разруливали этот код :)
Я обрабатывал асинхронность сообразно ситуации. То есть иногда достаточно одного callback-a (и ради него одного ничего не надо накручивать), иногда делал вызов синхронным с возвратом того результата, который отдаётся callback-у, иногда просто поток отдельный запускал. Но всегда было понимание контекста — зачем я это делаю. Контекст задаёт ограничения, которых не задаёт абстрактный пример. Поэтому абстрактный пример как не напиши — всегда можно сказать, что на самом-то деле могут быть вот такие ограничения, и тогда данное решение будет подходящим. А вот с заданным контекстом никто уже не отвертится, потому что ограничения уже задекларированы.

Поэтому я и говорил про более конкретный пример с понятным контекстом.
> То есть иногда достаточно одного callback-a
Это очень странно. Обычно асинхронный код распространяется по исходникам как короновирус, и одним методом тут как правило отделаться не получается
> иногда делал вызов синхронным
Вы же понимаете, что это не очень хорошо?
> иногда просто поток отдельный запускал
Тоже метод, но думаю, вы понимаете его ограничения.

> А вот с заданным контекстом никто уже не отвертится, потому что ограничения уже задекларированы.
А потом приходит начальник и говорит «все поменялось, надо исправить». Знаем, проходили. Поэтому у меня не было здесь цели показать, как работает такое решение в проде. У меня была цель дать теоретическое введение в работу корутин

Давайте так. Что сподвигло меня написать эту статью. В языке C++ постепенно появляются корутины. Есть один очевидный способ использования корутин — создание генераторов. В принципе, я понимаю этот подход, и понимаю, чем он хорош. Но неужели целый нетривиальный механизм затащили в язык только для того, чтобы писать генераторы?

Я задумался над этим вопросом, и у меня возникла идея, что одной из возможностей корутин могла бы стать линеаризация существующего асинхронного кода. Что это означает? Под асинхронным кодом в данном контексте я подразумеваю код с использованием коллбэков. И при использовании коллбэков рано или поздно разработчики утыкаются в так называемый callback hell. Замедьте, этот термин не я придумал, эта ситуация известна куче программистов по всему миру. И я решил задаться вопросом, можно ли исправить этот callback hell корутинным подходом.

Что необходимо, чтобы организовать callback hell? Для этого нужны 2 вещи — собственно коллбэк и так называемый event loop, который «дернет» коллбэк при наступлении нужного события (при готовности ответа например). Я не знаю других механизмов, как это еще можно сделать (можно запустить отдельный поток, и в нем заблокировавшись ждать результат, и дождавшись, вызвать коллбэк), если вы знаете, то расскажите, но по крайней мере этот подход самый популярный. Я решил продемонстрировать этот подход, написав простенький акторный фреймворк. При этом у меня не было цели написать полноценный акторный фреймворк, что почемуто показалось eao197, акторный фреймворк служит лишь для демонстрации ситуации callback hell-а.

После того, как я продемонстрировал callback hell, я показал способ, как его исправить. При этом, у меня не было цели написать полностью готовое к продакшен решение. Даже сейчас там не все гладко с точки зрения продакшен-кода: нет проверок, нет обработки исключений. Я лишь хотел дать общее понимание, как работать с корутинами в данной ситуации. Для использования данного подхода на проде придется допиливать мое решение, в том числе приспосабливать его к конкретному фреймворку. Я полагаю, что это будет несложно после моего изложения, если я не прав, ну чтож, тогда это будет действительно справедливая критика.

Но еще раз, у меня не было цели показать в действии акторный подход. У меня была цель найти единомышленников, кто сталкивался с callback hell (а я сталкивался) и попробовать предложить им решение этой проблемы

У меня не было желания продолжать обсуждение, но раз уж вы сами меня упомянули, то вот почему я считаю, что использование "акторов" и "акторного подхода" в вашей статье неуместно: "[prog.actors] Почему я не считаю упомянутых в статье на Хабре акторов настоящими акторами".


И, повторюсь, проблема вашей статьи в том, что вы искусственно создаете ненужную сложность на пустом месте. И эта сложность мешает воспринимать то, что вы хотите рассказать.


Если вашей целью было показать callback hell, то и нужно было просто показывать callback hell, без приплетания "акторного" подхода, да еще с такой кривой интерпритацией и странной реализацией (странной даже со скидкой на то, что это всего лишь демонстрация). Пример этого самого callback hell можно увидеть по той же ссылке выше. Без всяких наколеночных акторных "фреймворков".

Да вот же:


thread_executor contextA; // Контекст для операций над A.
ExecutionContextBoundDataObjectA A(contextA); // Это "актор" A.

schedule(A, [&A] {
   auto a = A.getA();
   schedule(A, [a, &A] {
      auto b = A.getB();
      schedule(A, [a, b, &A] {
         A.saveAB(a - b, a + b);
         schedule(A, [&A] {...});
      });
   });
});

Типичный. Но читается проще, т.к. нет всех тех условностей, которые вы у себя в статье нагородили.

Это код? Это не код. Оно даже в функцию не обернуто. Не говоря уже о том, что я не понимаю, что делает функция schedule

Давайте всетаки доведем обсуждение до логического конца.


Поймите, я специально привел в статье полный код, провоцирующий callback hell. Это не баг, а фича так сказать. Вы же пока привели какойто кусок кода, который и вставить пока непонятно куда. И я также не понимаю, как работает функция shedule

Давайте всетаки доведем обсуждение до логического конца.

Подозреваю, что это вряд ли возможно.


Вы же пока привели какойто кусок кода, который и вставить пока непонятно куда.

Это аналог вот этого вашего фрагмента из самого начала вашей же статьи, увидев который неподготовленный читатель так же может заявить "Это код? Это не код. Оно даже в функцию не обернуто":


    abActor.getA(ABActor::GetACallback([this](int a) {
        abActor.getB(ABActor::GetBCallback([a, this](int b) {
            abActor.saveAB(a - b, a + b, ABActor::SaveABCallback([this](){
                abActor.getA(ABActor::GetACallback([this](int a) {
                    abActor.getB(ABActor::GetBCallback([a, this](int b) {
                        std::cout << "Result " << a << " " << b << std::endl;
                    }));
                }));
            }));
        }));
    }));

И я также не понимаю, как работает функция shedule

Она получает два параметра:


  • объект, из которого можно извлечь execution_context;
  • функтор, который нужно вызвать на execution_context из первого параметра.

Функция schedule извлекает из первого параметра execution_context. У этого execution_context есть очередь, в которуе schedule ставит заявку на выполнение функтора (который передается вторым параметром).


В данном примере используется некий thread_executor, который реализует execution_context в виде отдельной нити ОС.


Т.е. здесь принцип такой же, как в вашем наколеночном "акторном" фреймворке, но без всякой лишней шелухи, только сама суть.

> Это аналог вот этого вашего фрагмента из самого начала вашей же статьи
В начале статьи у меня было интро для превличения внимания, чтобы показать что из чего получается. Дальше шел полный разбор этого кода с реализацией.

А так как у вас полной демо-реализации не заметно, то я не представляю, как сравнить сложность двух подходов, на чем Вы настаиваете.
Ну ок, я примерно понял, как это работает. Давайте я тогда напишу небольшой класс, чтобы подсчитать сложность в строках кода

class Work: public Actor {
    std::optional<std::pair<int, int>> cache;
public:
    void work() {
         if (cache.has_value()) {
               std::cout << cache->first << cache->second;
               return;
         }
         int a = A.getA();
         shedule(A, [this, a](){
               int b = A.getB();
               cache = std::make_pair(a, b);
         });
    }
}

Это Ваш вариант. А вот мой вариант:
class Work: public Actor {
    std::optional<std::pair<int, int>> cache;
public:
    void work() {
         if (cache.has_value()) {
               std::cout << cache->first << cache->second;
               return;
         }
         A.getA(GetACallback([this](int a) {
               A.getB(GetBCallback([this](int b) {
                       cache = std::make_pair(a, b);
               });
        });
    }
}


Мне кажется, по количеству строк ровно то же самое, в чем сложность то?

(PS минусую не я, у меня вообще нету права голосовать)

Если стоит цель свести количество строк к минимуму, то "типа мой" вариант можно вообще записать вот так:


    void work() {
         if (cache.has_value()) {
               std::cout << cache->first << cache->second;
               return;
         }
         shedule(A, [this, a = A.getA()](){
               cache = std::make_pair(a, A.getB());
         });
    }

в чем сложность то?

Сложность в том, что в вашем примере читателю нужно разбираться вот с этой мешаниной из непонятно что делающих методов:


A.getA(GetACallback([this](int a)
A.getB(GetBCallback([this](int b)

Для демонстрации callback hell-а вызовы GetACallback/GetBCallback не нужны.

Вы даже не заметили 2 ловушки, оставленные мной в этом задании.


1) В каком потоке (то, что Вы называете execution_context) заполняется и используется кэш?
2) в каком потоке вызывается getA?

С чего вы решили, что я делал какое-то ваше задание? Это во-первых.


Во-вторых, для демонстрации проблемы callback hell это не имеет значения.


В-третьих, зачем мне продолжать этот разговор, если вы все еще ничего не поняли?

> С чего вы решили, что я делал какое-то ваше задание?
Ну ок, описался я. Не задание. Код.

> Во-вторых, для демонстрации проблемы callback hell это не имеет значения.
А что имеет? Ок, давайте рассмотрим Ваш первоначальный пример
schedule(A, [&A] {
   auto a = A.getA();
   schedule(A, [a, &A] {
      auto b = A.getB();
      schedule(A, [a, b, &A] {
         A.saveAB(a - b, a + b);
         schedule(A, [&A] {...});
      });
   });
});


Чем он будте отличаться от того, что если его упростить таким образом:
schedule(A, [&A] {
   auto a = A.getA();
   auto b = A.getB();
   A.saveAB(a - b, a + b);
});

Ой, смотрите, callback hell пропал. Примера не получилось.

В третьих, чего именно я не понял?

В четвертых, вы любите упоминать какието «кишки». А метод schedule не является из Вашего примера «кишками»?
Чем он будте отличаться от того, что если его упростить таким образом:

Этот пример всего лишь калька с того, что вы сами показали в своей статье, а именно — два раздельных обращения getA и getB к одному и тому же "актору":


abActor.getA(ABActor::GetACallback([this](int a) {
        abActor.getB(ABActor::GetBCallback([a, this](int b) {

Я хз почему вы так сделали. Но раз сделали, то это же я повторил и у себя.


Но если для вас это повод поймать собеседника на слове, то давайте перепишем то, что вам не понравилось, вот так:


schedule(A, [&A, this] {
  schedule(*this, [&A, this, a = A.getA()] {
    schedule(A, [&A, this, a] {
      schedule(this, [&A, this, a, b = A.getB()] {
        schedule(A, [&A, this, a, b] {
          A.saveAB(a - b, a + b);
          schedule(A, [&A] {...});
        });
      });
    });
  });
});

Смысла здесь больше не станет, но зато из кода явным образом будет видно, какие действия куда распределяются.


А метод schedule не является из Вашего примера «кишками»?

Нет, здесь schedule — это тот минимум, без которого статья теряет смысл. Т.к. вам нужно показать, что заданные пользователем лямбды отправляются на работу на какой-то другой контекст. А их результат может быть получен когда-то в будущем.


Тогда как вызовы GetACallback — это и есть кишки вашего наколеночного "акторного" фреймворка, в которые вы обязаны погрузить читателя, иначе ваш механизм диспетчеризации заявок не будет работать.

В моем коде есть несколько гарантий, который даются бесплатно с точки зрения проектирования. Одна из них — это то, что внутри лямбды не будет гонок данных, как бы не стремился к этому программист. В Вашем же коде гонку данных получить как нечего делать. Я Вам это выше продемонстрировал в 5-ти строках кода.

Теперь же, когда вы переписали свой код эквивалентно моему коду, согласитесь, совсем не очевидно, что стало проще, чего Вы так хотели добиться. К томуже, одна мелкая ошибка там все равно есть.

Да, я понимаю, что Ваш подход тоже имеет место быть. Возможно, он лучше в ситуации, когда программисту нужно иметь прямой доступ к настройкам перфоманса, когда каждая наносекунда выполнения на счету. Поэтому я не буду критиковать Ваш код (хотя я все равно не понимаю, где такое может потребоваться). Но и Вы, пожалуйста, не критикуйте мой. В моем коде просто банально другие гарантии надежности. Ну тоесть критикуйте, конечно, но предметно.
Вот если я захочу покритиковать Ваш код, я спрошу «а что Вы будете делать с приведенной выше гонкой данных», например. Могу еще немало примеров для критики набросать.

Вы с таким же успехом могли спросить меня «какого хрена вы исползуете непонятные корутины вместо вызова setContext, getContext», и как при этом я должен был бы Вам отвечать?

Про «теряет смысл, не теряет смысл». Я также не понял. И мой, и Ваш подход теряет смысл демонстрации callback hell, если в моем случае удалить из него Callback, а в вашем — shedule. Так что я не вижу разницы.
Но зато в моем подходе я могу показать «неотвратимость» наказания. То есть я говорю читателям — «смотрите, есть такой код, от него не отвертеться». Причем я не поясняю, откуда такой код взялся. Может, это легаси, может наоборот правильная архитектурная задумка. Не отвертеться и все. С этим надо жить. И это не требует пояснений. Это как использование асинхронного http например. От него не отвертеться. Да, можно полностью перерефакторить код на использование синхронного http, но думаю Вы и читатели понимают некоторые проблемы этого. А так — вот у вас есть асинхронный http, старый код вы трогать не можете, новый пилите. Вот вам таска, идите выполняйте. С этим думаю каждый знаком.

А в Вашем коде нужно долго и упорно пояснять читателю, откуда здесь взялся shedule, почему от него нельзя избавиться. В вашем первом примере shedule вообще в 90% мест был не нужен, как я бы это пояснил читателям? Приведите мне полную цитату. Вот такое легаси? Ну давайте уберем это shedule, ничего не измениться, задача решена, можно публиковать статью. В вашем текущем примере от shedule уже не избавиться, но тут вылезают другие проблемы, которые точно также придется разъяснять читателям.

То есть я не вижу, чем конкретно Ваш подход было бы проще объяснить «читателям» статьи. Можете попробовать набросать целиком формулировку объяснения, посчитаем по словам, чтоли. Чтото мне подсказывает, что выйдет примерно также, может чуть лучше, может чуть хуже (мне кажется, хуже, но спорить не буду). И по итогу этих действий, даже если выяснится, что ваша формулировка на 1 слово короче (очень сомневаюсь), действительно ли это повод критиковать мой метод построения повествования?

Уже задолбало ходить по кругу, но давайте еще раз напоследок. Вдруг до кого-то дойдет.


Нет никакого "моего подхода". Есть ваше решение и есть ваше описание вашего решения.


Это решение в том виде, в котором оно написано и описано, вызывает ощущение чрезмерной сложности. Что затрудняет восприятие вашей статьи.


Все.


Если вы с этим не согласны, то ничего не поделать.


Только, имхо, вам следовало бы подумать не о том, как из-зо всех сил защищать свое решение. А о том, что если уж вам говорят, что ваша статья читается с трудом, что это неспроста же ж. И что можно подумать о том, как сделать ее проще.


То, что вы не понимаете, как проще — это не проблема. Сейчас не понимаете. Через какое-то время, если задасться этой целью, может быть и придет решение.

Если автор статьи всё переусложнил, то вы всё переупростили и никак не видите этого.


У автора getA — это асинхронный метод, который не может вернуть результат. Вместо этого он принимает колбек.


У вас это синхронный метод, которому какой-то там schedule как пятое колесо телеге.


Правильный код, без лишних сложностей, но и без потери свойств, будет примерно таким:


// Контекст для операций над A.
auto contextA = std::make_shared<thread_executor> contextA(); 

// Это "актор" A.
auto A = std::make_shared<ExecutionContextBoundDataObjectA>(contextA);

A->getA([=] (auto a) {
    A->getB([=] (auto b) {
        A->saveAB(a-b, a+b, [=] () {
            A->getA([=] (auto a) {
                A->getB([=] (auto b) {
                    std::cout << "Result " << a << " " << b << std::endl;
                });
            });
        });
   });
});
Если автор статьи всё переусложнил, то вы всё переупростили и никак не видите этого.

Может быть. Но у меня нет ни времени, ни желания делать нормально то, что следовало бы сделать автору статьи.


У вас это синхронный метод, которому какой-то там schedule как пятое колесо телеге.

schedule нужен всего лишь для того, чтобы показать, что какие-то вещи нельзя делать на текущем контексте, а их нужно запланировать для выполнения на каком-то другом контексте.


У автора getA — это асинхронный метод, который не может вернуть результат.

Это проблема автора. Он никак не объяснил почему это именно так и почему это не может быть по-другому. Вы воспринимаете эту условность как само собой разумеющееся, а я нет. Почему ваше восприятие должно быть правильным — хз.

Может быть. Но у меня нет ни времени, ни желания делать нормально то, что следовало бы сделать автору статьи.

То есть донести вашу точку зрения до других у вас тоже нет желания? Тогда зачем вы тут пишете комментарии?


Вы воспринимаете эту условность как само собой разумеющееся, а я нет.

Я воспринимаю это как условие задачи. Будет другое условие — будет другое решение (и там не будет корутин).

То есть донести вашу точку зрения до других у вас тоже нет желания?

Свою точку зрения я донес. Но дальше начался процесс разбора того, что именно вызывает ощущение чрезмерной сложности и как от этого можно избавиться. А вот на такое занятие нужно гораздо больше сил и времени, чем у меня есть в наличии. Поэтому получается так, как получается.


Я воспринимаю это как условие задачи.

А я нет, поскольку автор не сделал себе труда нормально описать постановку задачи. По крайней мере так, чтобы читателям вроде меня она была понятна.

Ну так пройдите мимо, раз условия непонятны.


Если хоть раз в жизни приходилось писать асинхронный код — должно быть понятно откуда эти колбеки вылазят. Если не приходилось — то зачем вам лишняя информация?

Не говорите куда пройти людям и люди не будут говорить куда пройти вам. Намек, думаю, понятен?


Асинхронного кода довелось написать достаточно. Настолько, чтобы скептически отнестись к условиям, выдвигаемым автором статьи.

У вас здесь, насколько я понимаю задумку автора, тоже не правильно:


A->getA([=] (auto a) {
    A->getB([=] (auto b) {

Ибо одна из сторон проблемы, о которой он говорит, состоит в том, что getA должно выполниться на контексте А, тогда как лямбда, которая воспринимает результат getA, и в коротой инициируется getB, должна быть выполнена на другом контексте. Откуда у автора и возникает потребность в GetACallback/GetBCallback. Что в вашей цепочке никак не выражено.


Так что ваш "правильный" вариант является таковым только на ваш взгляд. И вы переупростили не меньше, чем я.

Да, результат коллбэка должен возвращаться в тот контекст, на котором этот коллбэк был создан

Вот этого как раз я в коде автора не увидел. У него только 1 актор же, и весь код будет выполняться в его контексте. Разве что может понадобиться стартовый schedule добавить.

Тем не менее — это одна из особенностей решения автора статьи. И один из камней, о который я споткнулся изначально, не понимая долгое время суть GetACallback и спрятанной за ним механики.

Именно потому её я предлагаю убрать. Чтобы никто на ней не спотыкался.

По крайней мере это более конкретная претензия. Теперь я увидел, в чем у некоторых читателей возникают проблемы. Ок, возможно над этим стоило подумать. Мы же с Вами зачемто обсуждаем различие в 2-х подходах, кто лучше, кто хуже.

«Не понятно, что результат коллбэка выполняется в том месте, где коллбэк создан» — хорошая претензия, возьму на заметку. Хотя мне казалось, я детально пояснял в статье что и ка работает, ну чтож.
«Ваша статья переусложнена, нужно писать как-то по другому» — фиговая претензия, если я за 3 дня обсуждения не понял, в чем она состоит. Вы сами вменяете мне в вину, что я чтото не объяснил, а сами объяснить не хотите. Вот пришел человек и в 3 коротких сообщениях объяснил, что не так
Вы сами вменяете мне в вину, что я чтото не объяснил, а сами объяснить не хотите.

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


А вот читатель вовсе не обязан что-то расжевывать автору статьи и может, в принципе, ограничится фразой "что-то нифига не понятно". Все остальное — это добрая воля читателя.


Вот пришел человек и в 3 коротких сообщениях объяснил, что не так

Остается открытым вопрос были бы эти три коротких сообщения, если бы не вся предыдущая дискуссия до этого.

Ну мнеже как автору тоже хочется понять, что не так? Ваше право — объяснять мне это или нет, мое — попытаться понять затруднения. И почемуто разговор с Вами досихпор не добавил мне понимания.

Обидно, что столько времени потрачено впустую. Я просто думал, что у Вас действительно есть какоето супер решение, которое все прояснит. Но я ваше решение не понимаю. Как и Вы мое. Честно. Не троллю, не придираюсь, не понимаю. И, равняя по себе, представляю себе, что и другой читататель не поймет

Я вам еще раз объясняю, что нет никакого моего решения.


Есть ощущение, что вы переусложнили свою статью и она читается тяжело.


Попытки донести до вас этот простой факт не помогают. Т.к. вы не желаете включить свою голову и посмотреть на свой же код/текст другими глазами.


А чтобы написать свое решение этой же задачи (которую вы, кстати, не удосужились сформулировать) нужно потратить в разы больше времени, чем уже было потрачено на комментарии в этой теме.

Судя по «независимым комментариям», я статью не переусложнил. А недообъяснил. Не пояснил, почему контекст лямбды должен выполняться на треде, где был создан. Если бы Вы мне это сказали, я бы это понял первым же сообщением.

Сейчас вы скажете, что Вы это и называете «переусложнил». Отлично. Сколько копий сломано из-за обсуждения значения слова, вместо того, чтобы просто взять и показать на проблему
Не пояснил, почему контекст лямбды должен выполняться на треде, где был создан.

Я все же придерживаюсь мнения, что для вашей статьи это было не нужно вообще. И для демонстрации короутин вы могли бы использовать пример кода, который показал mayorovp.


Но если вы готовы кидаться помидорами в человека, который попытался донести до вас недостатки вашей же работы, то Ok.

Вот в этом коде я вижу только abActor:


    abActor.getA(ABActor::GetACallback(*this, [this](int a) {
        abActor.getB(ABActor::GetBCallback(*this, [a, this](int b) {
            abActor.saveAB(a - b, a + b, ABActor::SaveABCallback(*this, [this](){
                abActor.getA(ABActor::GetACallback(*this, [this](int a) {
                    abActor.getB(ABActor::GetBCallback(*this, [a, this](int b) {
                        std::cout << "Result " << a << " " << b << std::endl;
                    }));
                }));
            }));
        }));
    }));

А он что, используется для чего-то кроме получения поля abActor? Ему тут не обязательно быть актором, и в его контексте ничего не исполняется.

В его контексте исполняется std::cout и вызов актора abActor.
Представьте, что вместо std::cout тут было бы сохранение значения в переменную класса this. В данном случае никакой «гонки данных» происходить не будет даже в случае многопоточного кода, так как коллбэк будет возвращаться в контекст того класса, где был порожден
А он что, используется для чего-то кроме получения поля abActor?

Нет. И из-за этого так же приходилось прикладывать усилия, чтобы разобраться что к чему.

Это лишь картинка для привлечения внимания. Если ее ктото не понял — ок, это камень в мой огород. По крайней мере мы за 4 сообщения добрались до корня непонимания проблемы, а я с товарищем выше уже третий день разговариваю

Проблема в том, что у вас "лишь картинка для привлечения внимания" занимает треть поста.

Обычно асинхронный код распространяется по исходникам как короновирус, и одним методом тут как правило отделаться не получается

Но вы же программист, вы должны контролировать свой код. Просто не распространяйте короновирус.
> иногда делал вызов синхронным
Вы же понимаете, что это не очень хорошо?

Почему нехорошо? Возьмём ваш линеаризованный вариант, вот первая строчка:
const int a = co_await actor.abActor.getAAsync();

В ней мы синхронно дожидаемся возврата значения а, то есть вы вы тоже сделали синхронный вызов. Почему же мне нельзя?
У меня была цель дать теоретическое введение в работу корутин

Но для этого пришлось объяснять читателю, как работает целый фреймворк. И на этом фоне собственно тема «как работают корутины» просто потерялась. Потому что для её понимания нужно продраться через понимание фреймворка, потом понять, как это относится к примеру, потом понять, где здесь корутины, и только потом понять как работают корутины (если получится не потерять нить объяснения гораздо раньше). Вот это и есть сложность, о которой и участник eao197 вам пишет.
у меня возникла идея, что одной из возможностей корутин могла бы стать линеаризация существующего асинхронного кода.

Вот! То есть на самом деле вы не объяснение работы корутин давали, а пример того, как с их помощью сделать код последовательным (ну и получается, что синхронным). Но смысл-то здесь простой — спрятать callback-и. То есть, например если мы работаем с оборудованием и ожидаем его готовности при помощи callback-а, который дёргает ось, то мы можем написать универсальную функцию, которая возвращает тот результат, что отдаёт ось callback-у, и отдаёт его синхронно, то есть для внешней вызывающей стороны callback не виден, а виден обычный синхронный вызов функции, которая прячет внутри все эти callback-и.

Ну и такой подход (с функцией), на мой взгляд, проще написания дополнительного фреймворка.

ЗЫ. Я тоже сталкивался с callback-hell, ну и обходил его показанным синхронным способом.
В ней мы синхронно дожидаемся возврата значения а, то есть вы вы тоже сделали синхронный вызов.

Нет, этот вызов только выглядит синхронным.

Не понял про синхронный способ. Вы использовали корутины? Корутины — это по прежднему асинхронный способ, хотя и выглядит как синхронный.
Вообще, это конечно особенности именования. Мне тоже не нравится, что код такого типа называется асинхронным, но что поделаешь.
Я понимаю, что можно написать небольшую обертку, которая будет до упора ждать, пока исполнится асинхронный код и продолжит синхронное выполнение. Но вы понимаете, что это идет вразрез с использованием того асинхронного кода? Если вы таким образом «синхронизируете» код, значит асинхронный код вам не нужен, зачем же вы его тогда взяли?

> То есть, например если мы работаем с оборудованием
То есть вместо написания простенького (как по мне) велосипеда вы предлагаете рассматривать особенность работы на какомто оборудование, которое возможно известно только вам и еще небольшой группе людей?
Если вы таким образом «синхронизируете» код, значит асинхронный код вам не нужен, зачем же вы его тогда взяли?

Я не имел других возможностей. Мне дано оборудование и его обёртка от оси. Далее нужно как-то удобно этим пользоваться. Вот я и сделал удобство в виде простого последовательного исполнения задачи. Что может быть проще?
вы предлагаете рассматривать особенность работы на какомто оборудование, которое возможно известно только вам и еще небольшой группе людей?

Ну его легко можно абстрагировать. Там всё тривиально — делаем запрос, а результат всегда отдаётся через callback (это так в оси её авторы сочинили). И вот этих ответов нужно дожидаться много раз, при чём вложенно, то есть дождавшись одного ответа в callback-е мы из него же должны создать ещё один callback, потом дождаться его результата, ну и там повторить всё сначала. В результате получается глубокая и неудобная вложенность. А разруливается она примерно как у вас — просто преобразуем в последовательность синхронных вызовов (для внешнего наблюдателя всё синхронно). Вопрос только в реализации разруливающего велосипеда.
Синхронизация может быть разной. «Синхронизировать» корутинами — не тоже самое, что синхронизировать, заблокировав поток. Если производитель поставил асинхронный api, значит это имело по собой какойто смысл.
Если производитель поставил асинхронный api, значит это имело по собой какойто смысл.

Ну смысл простой — пока ждём ответа оборудования можно попробовать выполнить какой-то другой код. Это логично для браузера, когда блокируя его единственный поток мы подвешиваем браузер с точки зрения пользователя. Но если поток не единственный, то сразу исчезают подобные ограничения. И при этом раньше, чем пройдут все этапы ожидания ответов от оборудования мы всё равно не сможем продолжать что-то делать. То есть здесь абсолютно последовательная обработка команд. Ну и логично её оформить именно как последовательную, без callback-ов вообще.

Ну и так, до кучи, производители бывает такого напоставляют, что думать о смыслах вообще не приходится. Про некоторые китайские поделия я не нахожу других слов, кроме «идиоты». Хотя политкорректность требует промолчать…
Но если поток не единственный, то сразу исчезают подобные ограничения.

А если экземпляров "оборудования" — пара тысяч? Что, на каждый по потоку заводить будете?

Вообще-то речь о реальном железе. Поэтому «пара тысяч» — это явный натяг. Но ради чего?

Если были бы некие софтовые сущности в количествах, сопоставимых с вашим, тогда был бы другой разговор. Но здесь опять нужен всё тот же контекст — что за сущности, в каких процессах участвуют, какие ограничения и т.д. Если вы всё это пропускаете, но заявляете лишь о «наличии чего-то», то я вам уверенно отвечаю — для «чего-то» всегда есть «какое-то» решение. Надеюсь вас устраивает столь конкретный ответ? Точно так же меня «устраивает» ваш «конкретный» вопрос.

Какая разница, что там за сущности? Достаточно того, что они существуют и иногда встречаются в количествах намного больше указанных мною. Задачи-то разные бывают, и если вы с чем-то подобным не сталкивались — это ещё не значит, что ничего такого не существует.


Ближе к вашей области — вспомните про IOT. Представьте, что вы пишете MQTT-сервер, который будет собирать телеметрию со всех электрических розеток на заводе.

Какая разница, что там за сущности?

Ну если программу писать не надо, то никакой разницы.
Представьте, что вы пишете MQTT-сервер, который будет собирать телеметрию со всех электрических розеток на заводе.

Это простейший вариант шаблона «очередь». Много поставщиков событий на изменение состояния и (если нужно) много разбирающих события обработчиков. Опять без callback-ов.

И что вы этим так таинственно хотели сказать?

А протокол MQTT, который хоть и lightweight, но вовсе не самый простой, вы как будете реализовывать?

То есть здесь вопросы задаёте только вы?

Отвыкайте от таких надменных привычек.
Only those users with full accounts are able to leave comments. Log in, please.