Comments 70
SwitchContext(&loop_context_, ¤t_fiber_->context_);
// 0
if (current_fiber_->future_) {
// 1
current_fiber_->future_.Subscribe(
[this, fiber = current_fiber_] {
fiber->future_ = nullptr;
run_queue_.push_back(fiber);
});
// 2
} else {
// 3
}
Не очень понял что происходит в этом месте.
Пусть мы поставили задачу в очередь.
Теперь мы ее извлекли и переключили контекст на эту задачу (в точке 0).
Так как мы это сделали первый раз, то идем в точку 1 и подписываемся на событие (пока не будем обращать внимание, что произойдет дальше). Когда событие произойдет, то мы добавим его снова в очередь, и когда-нибудь извлечем его и попадем в точку 0. Из нее мы перейдем в точку 3. Что дальше?
Как я понимаю, дальше мы вернемся в метод WaitFor и продолжим выполнение дальше, так как мы загрузили этот контекст. Но почему этого же не происходит в точке 2?
В точке 2 нет никакого возврата/переключения контекста.
Вы попадаете в точку 0 из задачи (из её WaitFor), задача установила future, поэтому работает ветка 1. Но эта ветка ничего не кладёт в очередь, а всего лишь подписывается на событие. И больше контекста задачи нет нигде в очереди; обратно вы никак не попадёте, пока не сработает событие и не пнёт задачу снова в очередь. А тогда переключение перед точкой 0 перейдёт обратно в задачу и всё. Обратно после WaitFor задача уже не переключается, ни в какую точку в цикле вы уже оттуда не попадёте.
В статье пропущен метод создания корутины.
В него передаётся коллбек на наш код.
В основном метод создания корутины делает всего две вещи:
Вызывает коллбек,
Переключение контекста в Loop.
То есть как я понимаю, выполнение цикла в Loop продолжается если только ктото вызывает SwitchContext. А если никто не вызывает, то и выполнения не будет. Или я чего-то не понял…
А еще проверки на пустую очередь нету
Конкретно этот цикл (в этом потоке процессора) — никак. Поток будет исполнять рабочую задачу. Оно в целом получается так, что саму задачу вы начинаете стартуя новый поток (или кладя в очередь для пула потоков) — и потом она так и движется, где-то переключаясь в ожидание, где-то возвращаясь в себя, пока не выполнится. А в самом конце она либо вернёт руль пулу потоков, либо завершится.
Ну и проверки на пустую очередь нет на слайде, но она, очевидно, есть в реальном коде. Скорее всего ждёт нового задания в очереди (либо через событие, либо вот так же через future).
А в целом, если рассматривать переключение контекста именно как такой хитрый goto, весь подход — это просто очень хорошо оформленный спагетти-код. Вся логика приложения в одном месте, всё ожидание — в другом, а поток прыгает туда-сюда и по сути с его точки зрения эта самая "лапша" и получается. И главный ключевой момент — как раз переключение контекста. Не многочисленные вызовы функций, которые оставляют кучу своего наследства на стеке, а множество стеков и такой замаскированный goto
А какой вариант предлагается вместо этого? Модель fork — можно пояснить, что это за модель?
Да
«сопрограммы» — правильный перевод слова «coroutines»
Другие переводчики видят, что в русском языке уже есть слово «рутина», а значит можно перевести как «корутина» (приставка ко- в данном случае означает тоже, что и приставка со-: общее участие, общая принадлежность).
Для Cooperative Multitasking (он же Non-Preemptive Multitasking) тоже два варианта перевода: совместная и кооперативная мультизадачность.
Ну и, если вам обидно за путанницу в русском языке, которая якобы отсутствует в английском, напомню, что у Subroutine есть синонимы в виде Subprogram, Routine, Procedure, Function, Method, так что путаница возникает не только в русском языке.
Другие переводчики видят, что в русском языке уже есть слово «рутина»В русском это слово не имеет отношения к терминам программирования. Как и другое созвучное слово, про которое справедливо сказали выше:
«Копрограмма» — это совсем из другой области. :)
Показательно, что в вики нет переадресации «корутина — сопрограмма», т.е. согласно вики слова «корутина» нет.
Procedure, Function, Method— имеют гораздо более узкое, четко определенное значение. Функция возвращает результат, а процедура нет, а метод это процедура или функция какого-либо класса (во всяком случае, так в ОО Паскале). Так что нет в английском путаницы, как и в грамотном русском-техническом. Нет никакой сложности перевода — термин Coroutine давно переведен и отражен в рувики.
если вам обидно за путанницуМне «до лампочки фиолетово» и совершенно не обидно, что в какой-то публикации проявилась элементарная безграмотность. Дело не в несуществующей обиде, а в том, что формат Хабра предполагает обсуждение статьи в виде комментов. В этих обсуждениях читатели зачастую высказывают пожелания по улучшению статьи, устранению в ней ошибок. Я, как и многие, указываю в комментах замеченные мной ошибки, и обычно авторы благодарят за желание помочь, хотя (естественно) не всегда соглашаются. Здесь, в отличие от многих других авторов Хабра, автор даже не ответил на мое замечание, ну что же — его право считать свою статью выше всякой критики — написал бы это в статье в явном виде: «статья идеальная, поэтому критика не принимается» — я бы тогда и не стал лезть со своим советом.
Несколько странно видеть такое количество молчаливо несогласных читателей с этим моим советом. Видимо, они за путаницу в терминологии, за искусственное усложнение языка, а некоторые даже за пароли типа QWERTY1234. Ok — каждый вправе выбирать для себя заблуждения без обсуждения и учета каких-либо мнений. Не надо только потом жаловаться, что уровень публикаций на Хабре падает.
Язык — подвижная штука, пресловутое кофе, которое когда-то было мужского рода, все знают. Кофе существовало веками, а корутины многие люди открыли только вчера. Зачем тащить легаси слово «сопрограммы»? Если бы его кто-то использовал и оно было необходимо для понимания — может быть. Но его никто не использует. То есть можно выбрать, какое из слов использовать. Я выбираю «корутины», потому что это глобальная международная терминология, которую поймут где угодно в мире, одно-единственное слово на все популярные языки.
Я ни разу в жизни не слышал в бытовой речи слово «сопрограмма».Как интересно: а я ни разу не слышал слово «корутины», все говорят «сопрограмма», если говорят на русском. И авторы рувики говорят «сопрограмма». Термин укоренился со времен СССР см., нпр., руководство к процессору Электроника 60. И речь не о бытовой речи — в быту я могу сказать, что "в кейсе как попка клаву топтать кончишь фейсом об тэйбл", но согласитесь, такое высказывание явно не для статьи.
Терминология может быть международной, но это слово вполне себе переводится. Корутины — так, конечно, можно говорить, но это жаргонизм, примерно как «шедулить» вместо «планировать», «заводить баги» вместо «создавать задачи/дефекты». Т.е. понять могут, но по-русски это несколько безграмотно.
Вы почти написали async/await из TPL. Вообще, конечно, аналогии с шарпом не помешали бы. Его многие знают :)
Интересно, где бы был nginx, как платформа, если б не гнался за копейками и внедрил, скажу тоосто, сразу Rust. Я еще помню, как больно было лет пять назад писать модули для nginx без этих вот абстракций на голом Си (вздыхает). Неужели это правда необходимо, как пишет автор статьи?
Его бы не было.
Он как раз примечателен тем, что один из первых решил для массового пользователя задачу 10К, причём весьма изящно и эффективно, ещё в те времена, когда хрестоматийного метода решения подобных задач ещё не было. И заодно допилил средства, чтобы подобные задачи было решать удобно. Игорь Сысоев же не только сам nginx написал, но ещё и к частям ОС, которые были в том состоянии для решения этой задачи неудобны руку приложил. Нагуглите про разницу между epoll и kqueue, например. До сих пор одним из самых исчерпывающих и понятных материалов находится ответ Игоря на эту тему в старой почтовой эхе или на форуме.
И тем самым nginx стал референсом. Когда такая задача возникала где-то ещё — говорили про nginx и смотрели, как сделано там. Уверен, что и разработчики rust тоже смотрели на него же.
И, кстати, ни за какими копейками он в то время не гнался. Это уже современная модель, она позже появилась.
и внедрил, скажу тоосто, сразу Rust.
И как, по вашему, должен выглядеть этот перешедший на rust nginx? Что конкретно вы хотите поменять и на что.
после строчек read и white поток выполнения
Здесь, видимо, опечатка: white -> write
На хабре внедрили интересную фичу — Отправка сообщений об опечатках в публикациях. Проверено — работает ;)
В случае, когда мы писали синхронный код, мы скрыли вопрос полностью под капот и сказали, что этим будет заниматься операционная система, разрешили ей прерывать и перепланировать наш потоки выполнения.
Хорошо.
В язык программирования нужно добавить конструкции, позволяющие выражать темпоральные связи между событиями — в текущем моменте времени и в неопределенном моменте в будущем.
И бам, фундаментальная ошибка в логике. И виной тому размытые понятия. С(и С++) не относится к тем языках, которые понимаются под языками выше. С++ может управлять потоком выполнения и никакой ОС для этого ему ненужна.
Такие же ошибки существуют и выше, когда так же путаются понятия. select/epoll — это синхронные api, никакая асинхронность никакой эффективности не даёт. epoll + неблокирующие сокеты — это именно взлом абстракции, неэффективной абстракции(sockets api). Мы имеем на уровне сети один поток, на уровне ОС один поток, на уровне сокетов много. Это проблема.
Мультиплексинг занимается именно объединением этого «много» опять в один поток. По-сути мы попросту выкидываем.
nginx же написан подобным образом не потому, что ему нужна какая-то асинхронность на уровне io(ведь мультиплексер(epoll) делает io опять СИНХРОННЫМ). Все эти события/реакции — это именно внутренняя архитектура nginx, а io в неё попросту интегрировано.
Но это Scala, а что делать нам, С++ разработчикам?
Ничего — скала не может управлять потоком исполнения. С++ может. Все эти абстракции тут попросту ненужны.
6. Как сделать код читаемее с помощью корутин
И только с этого шага говорится о чём-то актуальном для С++.
Тогда мы реформируем код таким образом:
Код не очень хорош. Контекст передаётся неявно, а значит все эти WaitFor попросту нагромождения ненужных абстракций.
6.1. Корутины
И именно это трувей.
boost::fiber
А этот код уже хорош. Там нет(т.к. ненужно) ничего из того, что описывалось выше.
Чтобы доработать конструкцию до рабочего состояния, нужно как-то оперировать запланированными задачами. Обычно планируемую задачу называют Fiber — логический поток выполнения. Мы её будем представлять как пару контекст+Future. Для того, чтобы привязывать все конструкции с ожиданием, будем хранить в каждом логическом потоке выполнения ту Future, которую он сейчас ждет.
Автор всё не унимается и пытается натянуть неудачные(в контексте С++) абстракции на С++. Зачем, если автор знает про boost::fiber?
Мы сможем написать код, который выглядит как синхронный, в нем явно размечены точки ожидания, они хорошо читаются.
Это причина всего этого нагромождения неактуальных вещей? Ну дак это глупость, ведь смысл синхронного кода в том, что он выглядит так, будто-бы в нём нету ожидания. Зачем это выделять? Какой в этом смысл?
Можно ли сделать лучше?
Да. boost::fiber. В С++ можно реализовать на уровне языка прозрачное блокирующие api и именно это сделано в boost::fiber. Зачем и для чего нужна вся эта борьба с мельницами — неясно.
Когда будет Coroutine TS, можно будет убрать скобочки в коде и сказать, что WaitFor и CoroutineWait, который получается из CoroutineTS — это более-менее одинаковые сущности.
Нет.
Это явно размеченные точки ожидания, где нам нужно прерывать текущий поток выполнения и чего-то дождаться.
Нет.
Нужно понимать, что «CoroutineTS» это именно код, находящий в контексте «нельзя прерывать поток выполнения», т.е. всё возвращается к тому, что С++ становится скалой/жаваскриптом. И именно поэтому там такой api, а не потому что это попытка сделать лучше(то, что показано выше. Что показано выше лучше не сделать и так делать вообще ненужно).
К тому же, семантика «CoroutineTS» намного глубже и он полностью изменяет семантику функций. От того он и заменяет базовые/добавляет новые ключевые слова. С показанными выше изваяниям это так же никак не связано.
Именно поэтому все сетевые вещи типа веб-серверов строятся как асинхронный код,
Да не строят их как какой-то асинхронный код. Это обычная логика. Никто нигде и не обязан отделять какие-то там отдельные соединения в отдельные же потоки. Это всё накрученные сверху абстракции.
Если у вас не очень нагруженное приложение, и вам достаточно этого уровня, то дальше идти не нужно, вы можете здесь остаться, и все будет хорошо.
Там всё будет хорошо в любом случае. Существуют единицы приложений, которые действительно от этого пострадают, особенно в контексте сравнения с жаваскриптов/скалой.
Если вы автор nginx, у вас, к сожалению, тернистый путь, вам нужно явно работать с низкими уровнями, вписать этот сложный код. И если вы хотите максимально уменьшать накладные расходы, то вы срезаете все абстракции, в частности, не используете никаких future и promises.
Опять же — размывание понятий. Всякие «future и promises» не используются не потому, что нужно срезать какие-то абстракции — просто эти абстракции из совершенно другого мира и подхода.
Тот же coroTS никак тому же nginx не помешает, как и любые другие адекватные подходу абстракции.
Тогда вам будут удобны абстракции типа futures, promises и actors. Это может сэкономить время. При этом эти абстракции можно более глубоко интегрировать в язык за счет сопрограмм, как я постарался проиллюстрировать.
Зачем? Есть потоки. Не хватает потоков — есть юзерспейс потоки с той же семантикой. Никакого лишнего мусора ненужно — бери boost::fiber и пользуйся.
Какие-то заморочки нужны в случае предыдущем.
Что-то вы сами себе противоречите...
Опять же — размывание понятий. Всякие «future и promises» не используются не потому, что нужно срезать какие-то абстракции — просто эти абстракции из совершенно другого мира и подхода.
Тот же coroTS никак тому же nginx не помешает, как и любые другие адекватные подходу абстракции.
А ничего, что сопрограммы основаны на «future и promises»?
Вообще говоря, сопрограммы и future/promise — вещи ортогональные. Можно реализовать сопрограммы без использования future/promise подхода (см. synca: habr.com/ru/post/201826), обратное тоже верно. Можно их скомбинировать и получить сборную солянку, как в вышеприведенной статье.
Однако, как мне кажется, такая солянка ясности это не добавляет. Даже наоборот.
Конкретно CoroutineTS основан на специальных объектах, которые очень похоже на future/promise, но при детальном рассмотрении — совсем не похожи.
Для этого рассмотрим интерфейсы:
template <typename T>
struct my_future {
bool await_ready();
void await_suspend(std::experimental::coroutine_handle<>);
T await_resume();
};
Легко видеть, что тут нет никаких then
, onError
и т.д. Т.е. это похоже на future/promise примерно так же, как Java похожа на Python.
Основная идея future/promise — это композиция вычислений. Мне очень интересно, как с помощью этого интерфейса можно реализовать что-то типа этого:
// Вычисление (2v+1)^2
Future<int> anotherValue = value
.Then([] (int v) { return 2 * v; })
.Then([] (int u) { return u + 1; })
.Then([] (int w) { return w * w; });
Тот факт, что then возвращает новый promise — всего лишь приятный бонус.
Основная идея promise — это разделение вызова асинхронного метода и передачи колбека, а также возможность долговременного хранения промежуточного результата.
Я уже отвечал на это — это фундаментально неверное утверждение в контексте coroTS, т.к. там вся эта логика с названием «future»(на самом деле promise) и иже с ним никакого отношения к «методу», «калбеку», «хренению чего-то» не относится.
Это именно управляющая структура в которой описывается конфигурация корутины и операции управления ею. Что делать в одном случае, а что в другом. Контекст там вообще в coroutine_handle лежит.
Промисом оно называется потому, что это типа некий интерфейс доступа к будущим вычислениям. Просто взяли привычный базоврд. К «future и promises» определённых в рамках С++ — оно отношения не имеет, да и тому же самому в остальных(как минимум упомянутых тут) языках тоже.
Ок, тогда просто прошу реализовать это:
// Вычисление (2v+1)^2
Future<int> anotherValue = value
.Then([] (int v) { return 2 * v; });
Внезапно окажется, что await_suspend
и coroutine_handle
— это вовсе не клиентская часть для создания продолжений, я часть, связанная с жизненным циклом сопрограммы.
А ничего, что сопрограммы основаны на «future и promises»?
Выше gridem уже ответил, и ответил правильно. Да и я об этом упоминал. coroTS — это по-сути новая семантика для функции. Она превращает функцию в некий много раз вызываемый генератор, выглядит это как-то так:
auto f = [](auto cb) {
for(size_t i = 0; i < 10; ++i) cb(i);
};
auto coro_f = [i = 0ul]() mutable {
if(i < 10) return i++;
return ~0ul;
};
Это очень упрощенный пример, но примерно так оно и выглядит. Ведь что такое корутина — это просто функция, выполнение которой можно в любой момент остановить, а после продолжить.
Так же, в контексте С++ future/promise — это всё про потоки(просто переключение контекста, про стек в этом контексте). А nginx и coroTS(на выходе) — это именно про логику, логику в рамках одного контекста.
Почему наружу из coroTS торчат future и promises? Ну это просто модные/привычные слова. Как было сказано ниже — это управляющая структура. Она никак не связана с
позволяющие выражать темпоральные связи между событиями — в текущем моменте времени и в неопределенном моменте в будущем.
И бам, фундаментальная ошибка в логике. И виной тому размытые понятия. С(и С++) не относится к тем языках, которые понимаются под языками выше. С++ может управлять потоком выполнения и никакой ОС для этого ему ненужна.
Что вы понимаете под "может управлять потоком управления"?
Что вы понимаете под «может управлять потоком управления»?
Всё очень просто. Есть некий поток исполнения и так же он является потоком управления, т.к. исполнение кода может изменять то — куда дальше пойдёт исполнение.
В базовой теории управляют этим всякие конструкции вида if/call/return/break и прочее. Всё управление явно описано и код исполняется в том порядке, в котором описано.
С++(и любой язык с подобными возможностями) попросту ломает лексическую семантику кода. Т.е. С++ может в любой момент остановить поток выполнения, прыгнуть куда угодно, сделать что угодно вне управляющих конструкций(описанных выше).
Поэтому тут возможно сделать read() который попросту стопнет выполнение. Хотя в базовой теории подобное вообще невозможно и именно поэтому там и вводятся всякие управляющие конструкции.
А зачем быть управляющей конструкцией для того чтобы останавливать выполнение?
Кстати, держите: fs.readFileSync
А зачем быть управляющей конструкцией для того чтобы останавливать выполнение?
Оно не может остановить, ведь я говорил, что в базовой теории этого нет. Оно не останавливает, а управляет.
Кстати, держите: fs.readFileSync
Ещё одна фундаментальная ошибка — это С++-функция и никакого отношения к js не имеет. Нужно показать именно функция написанную на языке, в рамках самого языка. А то, что можно из вне стопнуть исполнение — это всем известно, ОС поступает именно так же. vm для js является именно ОС, и стопает ОС.
Я же говорил об остановках в рамках самого языка, а не в рамках внешнего воздействия.
Ну так ноде.жс с плюсовым кодом — тоже просто нативный рантайм.
Неверно — это не нативный, а внешний рантайм. Нативный — это родной для языка рантайм, и родной не в смысле рядом, а всмысле как следствие написания кода на этом языке.
Кстати, делает ли наличие FFI в C в стандарте хаскеля сишечку частью хаскеля?
Очень слабо, т.к. это попытка выдрать один из критерием и пытаться его опровергнуть. Это изначально определяется за глупость.
По поводу аналогий с ffi — это так же глупость и по многим причинам. Во-первых никакой ffi ничего не может, т.к. выполнять он будет в контексте Си, а не в контексте хаскеля. Т.е. никаким образом этот код не позволит остановить именно хаскель-код.
Далее, никакого языка как ассемблер нет — это фантом. Даже если будут предприняты какие-то попытки назвать его языком — они так же обречены на провал, т.к. это язык(если это язык) совершенно другого уровня.
Точно так же, ассемблер не является языком, т.к. является базовым языком. Т.е. по-сути это нечто общение, присуще всему. А подобные общие вещи нельзя идентифицировать под что-то конкретное, что-то альтерантивное — оно безальтернативно и вообще из другого мира.
К тому же, как я уже говорил — стандарт меня никак не волнует и волновать не должен. Это какая-то попытка защиты от моего оппонента. Я нигде не декларировал что меня, да и вообще кого-то должно это волновать. Да и нигде и никто этого не декларировал, только задним числом и то только для меня, но не для себя.
К тому же, сами эти рассуждения — глупая попытка подменить понятия. Я нигде не говорил о том, что ассемблер является частью си. Я говорил о том, что кейворд asm, т.е. использование ассемблера — является стандартным для си.
Тут нужно ещё раз повторить. asm является базовым интерфейсом платформы. Он никак не может определяться как какой-то отдельный язык и т.п. Кто угодно может использовать базовый интерфейс платформы. Это не запрещается. Этому не может быть аналогий использованию ДРУГОГО языка.
К тому же, ffi предлагает использование ГОТОВОГО кода, а не написания кода. Это вообще не является эквивалентом. asm — это именно встроенный в си асм, встроенный асм. Это декларируется как часть си-кода. ffi не предполагает встраивания си-кода в хаскель, либо куда ещё.
К тому же существует setjmp/longjmp. Формально остановка уже есть, правда со стеком всё сложно. Но LRV это полнофункциональная фича.
asm — не часть языка, а зависимое от компилятора расширение. Стандарт языка не предписывает для него определенного синтаксиса и поведения.
К тому же существует setjmp/longjmp.
… являющиеся частью стандартной библиотеки, и невозможные для реализации средствами языка.
Ваша фундаментальная ошибка — в том, что вы изначально выбрали С++ в качестве "языка который может", а такие языки как Javascript и, внезапно, Scala — в качестве "языков которые не могут", вывели из этого разделения следствия, а теперь пытаетесь доказать исходное разделение пользуясь этими следствиями.
Хотя на самом деле всё отличие в том, что С++ — многопоточный, Javascript — однопоточный, а Scala вообще пытается быть функциональной.
А потоком управления могут управлять все языки программирования.
asm — не часть языка, а зависимое от компилятора расширение. Стандарт языка не предписывает для него определенного синтаксиса и поведения.
И? Он есть, какое там поведение к делу не относится. К тому же, я заранее свёл на нет все эти попытки юлить в сторону «не определено» тем, что nodejs и есть то самое нестандартное расширение. И у вас два пути. Играть в дурака, игнорируя это обстоятельство. Удалить изначальный коммент как несостоятельный. Либо — принять расширения за язык.
К тому же, все эти рассуждения полная глупость т.к. я нигде не говорил и каком-то языке без расширений — мне он не интересен, как и кому-либо ещё. Именно поэтому вы и начали ссылаться на ноду, а после начали вести двойную игру.
… являющиеся частью стандартной библиотеки, и невозможные для реализации средствами языка.
Опять попытка дёргать левые фразы. Где ответ на LRV? К тому же, вам никто не запрещал используя части стандартной библиотеки реализовать остановку на read() в js.
Ваша фундаментальная ошибка — в том, что вы изначально выбрали С++
Ну дак я выбрал, а после того как я выбрал — мы видим результат. Ваши глупые попытки мунисовать, гадить в карму, игнорировать неудобные тезисы и обстоятельства, вести попросту двойную игру.
а такие языки как Javascript и, внезапно, Scala — в качестве «языков которые не могут», вывели из этого разделения следствия, а теперь пытаетесь доказать исходное разделение пользуясь этими следствиями.
Дак я могу, я и доказал. Именно поэтому у меня есть на С/С++ реализация юзерспейс тредов, а в скале/жс нет. Это аксиома.
Хотя на самом деле всё отличие в том, что С++ — многопоточный, Javascript — однопоточный, а Scala вообще пытается быть функциональной.
Абсолютно неверно. Что значит неведанное «многопоточный» и какого оно имеет отношение к теме — неясно.
Многопоточность это и есть возможность управления потоком управления/выполнения. Когда возможно в какой угодно последовательности и как угодно(хоть в параллель) исполнять код. Кол-во исполнителей вообще никак и на на что не влияет. Фундаментальным является то, что я назвал фундаментальным.
А потоком управления могут управлять все языки программирования.
Вам дали задачу — вы не смогли её решить. О чём можно ещё говорить?
К тому же, тут можно заметить попытку подменить понятие. А ведь я явно говорил о том, что понимаю(вернее о какой части управлять говорю) под «управлять». И я говорил именно об управлении вне управляющих конструкций.
Очевидно, что никакой язык с внешним рантаймом и логикой на лексике в это не может, а такие почти все.
Параллелизм - тоже многозадачность. А то, что вы имеете в виду - это конкурентность (concurrency). Тут наглядно про это рассказывается: https://habr.com/ru/companies/piter/articles/274569/
Future<Request> GetRequest();
Future<Payload> QueryBackend(Future<Request> req);
Future<Response> HandlePayload(Future<Payload> pld);
Future<void> Reply(Future<Request> req, Future<Response> rsp);
Теперь Then не нужен, и функция хорошо выглядит даже без корутин:
auto req = GetRequest();
auto pld = QueryBackend(req);
auto rsp = HandlePayload(pld);
Reply(req, rsp);
Асинхронность в программировании