В предыдущей статье я описал простой путь создания генераторов на корутинах С++. На мой взгляд генераторы неплохо демонстрируют работу с такими объектами как coroutine_handle и promise_type. На этот раз речь пойдет об awaitable объектах — еще одной неотъемлемой части поддержки корутин в С++. А рассматривать мы их будем на примере реализации каналов, аналогичных каналам в GoLang. Как С++ разработчик, я не в восторге от многих решений принятых в GoLang, но в их каналы влюбился с первого взгляда. Итак, приступим! Я задался вопросом: можно ли сделать в C++ что-нибудь, что будет похоже на каналы в GoLang? Простое чтение и запись каналов — задачи тривиальные, поэтому я решил: «а не замахнуться ли нам на Вильяма нашего Шекспира», то есть на select. За образец была взята следующая функция на GoLang: Первая задача, которая перед мной встала, — это вообразить, как всё это может выглядеть на C++. Итогом моих размышлений стали два варианта: через std::variant и через лямбды. Сразу оговорюсь, я не хотел использовать никаких #define. У каждого из этих вариантов есть свои плюсы и минусы. Огромный минус первого — большое количество «странного кода», на вроде std::get_if<std::string>(&result). Помимо его ужасного вида, он позволяет легко ошибиться в типах и не позволяет использовать совместно однотипные каналы. Конечно, вместо типов при вызове std::get_if можно использовать индексы, но это тоже не очень красивое решение — у вас возникает два источника истины: при вызове select и в условии. Стоит поменять порядок параметров в select и забыть поменять номера в if, и в лучшем случае ваш код перестанет собираться, а в худшем — вы просто сломаете логику приложения. «Почему же автор оставил этот убогий вариант?» — спросите вы. Ведь второй выглядит заметно привлекательнее! Тут соль в особенности совместной работы корутин и лямбд в С++, которая отняла несколько дней моей жизни. Да, лямбда может быть корутиной. Но дело в том, что лямбды, будучи корутинами, не захватывают значения, попавшие в замыкание. То есть всё, что было перечислено в квадратных скобках, пропадет после первого же co_await. Но при этом такой код скомпилируется и никакой tidy вам ничего не скажет :( К тому же, вариант с std::variant немного проще в реализации и по сути является подготовительным шагом для второго. Кто не догадался при чем тут std::variant, result — это std::variant<int, std::string>, но об этом позже. Как я писал выше, в данной статье речь в основном пойдет о реализации awaitable объектов. Если в предыдущей статье мы использовали и говорили о co_yield, в этой мы будем пользоваться co_await. Давайте немного разберемся в том, как он работает. Данного описания логики работы co_await будет достаточно для понимания рассматриваемого примера, но оно не претендует на полноту. Например, я не буду подробно описывать все возможные варианты реализации await_suspend. Помимо void он может возвращать еще bool и std::coroutine_handle<>. Но ни то ни другое мне еще ни разу не понадобилось. Интересный пример на эту тему можно посмотреть в описании std::noop_coroutine на cppreference.com. Так же я ничего не скажу об await_transform и перегрузке оператора co_await. Более полное описание можно найти на том же cppreference.com Оператор co_await в качестве параметра принимает awaitable объект. Awaitable — это объект (структура или класс), реализующий следующие методы: В  clang 12, на момент написания статьи, coroutine_handle и другие объекты и функции для работы с корутинами находились в пространстве имен std::experimental. Первым делом co_await вызывает метод await_ready(). Если await_ready() возвращает true, то никакого прерывания корутины не происходит, тут же вызывается await_resume() и его результат возвращается в качестве результата вызова co_await. Если же await_ready() возвращает false, вызывается метод await_suspend() и после его завершения выполнение корутины приостанавливается вплоть до вызова coroutine_handle::resume().  Если вы запустите корутину как обычную функцию, после завершения await_suspend() функция-корутина «как бы завершится» и вернет вам в нашем случае task в качестве результата. На самом деле состояние всех локальных переменных при этом будет сохранено в куче. Они будут ждать возобновления её работы. Ответственность за вызов resume целиком лежит на совести разработчика awaitable объекта. resume возобновляет корутину, вызывает метод await_resume() вашего awaitable объекта, результат которого опять же возвращается в корутину как результат вызова оператора co_await. Если вы используете обратные вызовы (callback), а корутины можно рассматривать как частный случай таких приложений, важно следить за тем, чтобы  callback (для корутин это coroutine_handle::resume()) вызывался В ЛЮБОМ СЛУЧАЕ. Даже если произошла ошибка, callback не должен быть проигнорирован. Для начала определимся, что нам надо и напишем пустые заготовки. И так, нам нужен некий класс task с promise_type для компиляции корутины, channel — это собственно наш канал и некий select, возвращающий awaitable объект. В нашей задаче task несет лишь вспомогательную функцию — объясняет компилятору какой promise_type использовать для корутины. Детали реализации task большой роли не играют. Вы можете заменить его на свой класс, со своей логикой. Поэтому я просто приведу его код с некоторыми пояснениями. Возможно в STL уже есть что-то готовое. Подскажите — благодарен буду. Но вот использовать generator из предыдущей статьи совместно с каналами из этой не получится. Проблема в фундаментальной разнице между co_yield и co_await. Образно говоря, co_yield — просто приостановка выполнения функции, которая может быть в любой момент возобновлена. А вот co_await — ожидание какого-то события. В общем случае вы не можете такое событие поторопить. Гипотетически попытка принудительного возобновления функции прерванной co_await должна приводить к блокировке текущего потока выполнения  — он будет ждать возникновения события. В однопоточном приложении это привело бы к зависанию всего приложения. Поэтому, если вам нужен генератор, использующий в своей логике co_await, вам придется обращаться к нему также через co_await, а не простым вызовом, как я это делал в своей статье. И это будет совсем другой генератор. Чтобы определиться с интерфейсом channel нам надо всё же написать те самые тривиальные запись и чтение каналов. Мне захотелось, чтобы они выглядели следующим образом: Теперь мы знаем что, во первых, channel — это шаблон. Во вторых, он имеет два вложенных класса in для записи и out для чтения. Чтение и запись — асинхронные операции, следовательно in и out должны реализовывать интерфейс awaitable. Кроме того, нам понадобится еще некий helper для реализации конструкции co_await output << value . У меня получилось следующее: В своих примерах я не всегда расставляю  везде, где это требует тот же tidy. Но для функций, возвращающих awaitable объекты или различные helper объекты, это палочка выручалочка, спасающая от забытых co_await или тех же операторов записи. Особенно легко ошибиться, когда co_await ничего не возвращает. Вы просто забываете написать co_await, и всё собирается, но ничего не работает. И наконец, заготовка на select. Здесь мы используем variadic template и формируем тип того самого std::variant. Поздравляю! Наш код имеет все шансы быть скомпилированным. Но не заработать :) Идея проста как мычание, пока в канале есть место — записываем данные в канал, пока в канале есть данные — читаем их из него без приостановки корутин. Если места или данных нет — складываем coroutine_handle в очереди и достаем их из очередей по мере того как данные вычитываются или наоборот появляются. Чтобы не загромождать статью кодом, приведу лишь реализацию класса in, класс out полностью аналогичен только без helper. В классе channel дописываем конструктор и свойства для хранения данных и очередей ожидающих своего часа корутин. Вот скажите мне, почему ни в одном STL контейнере нет метода pop(), который бы не только удалял, но и возвращал значение? Функция pop в моём примере делает как раз это. Вот мы и получили каналы, в которые можно писать и из которых можно читать при помощи co_await. Данное решение будет работать, но у него есть пара серьезных недостатков: Прямой вызов resume() из resume_in/resume_out может привести к неограниченному росту стека. Поэтому лучше делать такие вызовы через boost::asio::post или любой другой планировщик Вторая проблема касается helper в in::await_resume. Дело в том, что никто не гарантирует, что в промежутке между завершением этого метода и вызовом оператора << место в канале не закончится. Такой эффект можно легко получить следующей последовательностью co_await chan << co_await another_chan. Поэтому неплохо было бы знать, сколько объектов helper данного канала создано и резервировать под них место Вот мы и подошли к реализации select. Если channel::in и channel::out сами представляют из себя awaitable объекты, то select — это функция, которая возвращает awaitable объект. Но, прежде чем перейти к реализации методов, нам надо как-то сохранить разнотипные каналы, полученные в качестве параметров. Ничего лучшего для этой цели чем std::tuple я не знаю, поэтому его и использовал. Это было не сложно. Теперь давайте попробуем по очереди разобраться с методами awaitable и начнем с await_ready. В нём надо вызвать метод await_ready из всех переданных в select каналов. Тут нам на помощь приходит std::apply. Вообще, это функция для вызова некоторого функтора с передачей в него элементов std::tuple в качестве параметров. Но в нашем случае он используется чтобы развернуть std::tuple обратно в variadic template. Затем fold expressions из C++17 помогают сделать наш код лаконичным и простым. Для того, чтобы реализовать await_resume, нам надо вызвать await_resume из канала, метод await_ready из которого возвращает true, и положить результат в std::variant<Channels::value_type...>. Для этого мне пришлось написать еще один шаблон и добавить в channel::out шаблонный метод. Почти всё. Осталось реализовать await_suspend. Для этого надо положить полученный coroutine_handle во все каналы, переданные в select. А далее сделать так, чтобы только один канал смог разбудить остановленную при помощи select корутину. Для этого я воспользовался связкой std::shared_ptr/std::weak_ptr и переделал channel::m_out_queue. Теперь очередь содержит не coroutine_handle, а std::variant<coroutine_handle, std::weak_ptr<coroutine_handle>>. А чтобы эти weak_ptr не протухли раньше времени, добавляем std::shared_ptr<coroutine_handle> в свойства awaitable, возвращаемого функцией select. Когда нам надо разбудить "счастливчика", который дождался данных, мы перебираем очередь, пока не встретим живой std::weak_ptr содержащий непустой coroutine_handle По своей сути coroutine_handle — указатели и вполне могут принимать значение nullptr Вначале я не планировал описывать реализацию select на лямбдах. Но это оказалось довольно просто. Мне лишь понадобился дополнительный helper теперь уже для channel::out, который возвращается оператором >>. Сама функция select фактически не изменилась, только теперь наш await_resume не возвращает значение, а вызывает callback. К моему большому удивлению, вариант на лямбдах получился даже проще. Но пришел я к нему через std::variant. Можно еще от метода callback в channel::out избавиться, написав вместо него еще одну лямбду. Рад приветствовать тех, кто добрался до этих строк! Я описал не всё. Неплохо было бы объяснить компилятору, какой из вариантов select выбирать. Я сделал это через концепты — еще одну замечательную возможность C++20. Добавить планировщик и обернуть все resume() в конструкцию post([coro] () mutable { coro.resume(); }). Реализовать channel::close(). Впрочем, первое можно найти в моём github. А вот с close() всё не так просто однозначно, кстати как и в GoLang. Даже решение с лямбдами получилось не столь лаконичное как в GoLang - сложно тягаться со встроенными механизмами языка. Но у него есть один существенный плюс — оно однопоточное. Следовательно, при шаринге ресурсов между корутинами вам не придется думать о всевозможных mutex. Вы будете просто писать «однопоточный код». В заключении хочу сказать, что я прошел увлекательный квест пока писал код и эту статью. Надеюсь кому-то она помогла пройти его со мной рядом. Ставьте лайки, пишите комментарии, задавайте вопросы. Буду рад помочь или дополнить свой рассказ подробностями. За сим на сегодня всё. Chief of R&D