vak: (Default)
[personal profile] vak
В последнем стандарте языка Си++ 2020 появилась поддержка сопрограмм (coroutines). Я давно к ним присматриваюсь, в надежде применить для симуляции цифровой логики. При очередной попытке понять сопрограммы я натолкнулся на статью Дэвида Мазиереса, и она столкнула меня с мёртвой точки. Попробую ещё больше упростить здесь для начинающих.

Сопрограммы в языке Си++ это такие процедуры или функции, которые не завершаются до конца, и их можно продолжить. Выглядит сопрограмма как обычная функция, но с двумя отличиями:
  • У неё особый тип возвращаемого значения.
  • В её теле встречается оператор 'co_await'. Аргумент этого оператора тоже имеет специальный тип.
Типом возвращаемого значения сопрограммы может быть произвольный класс, внутри которого определен другой класс с фиксированным именем 'promise_type'. Методы этого класса подсказывают компилятору, как создавать сопрограмму, как завершать, и какое значение возвращать.

Оператор 'co_await' в теле сопрограммы приостанавливает её и возвращает управление в вызвавший (или продолживший) её код. Также могут встречаться операторы 'co_yield' и 'co_return'.

Вот пример простой сопрограммы, при каждом продолжении печатающей нарастающий счётчик:
co_void_t counter()
{
for (int i = 1; ; ++i) {
std::cout << "counter: " << i << std::endl;
co_await co_await_t{};
}
}
Вероятно, вам приходилось иметь дело с потоками std::thread или std::jthread. Сопрограммы - это не потоки. В отличие от потоков, они не используют никаких ресурсов операционной системы (наподобие pthreads). Также не стоит путать сопрограммы с fibers, или green threads, или традиционных вызовов setjmp()/longjmp(). В отличие от fibers, сопрограммы не имеют собственного стека. Они выполняются на стеке вызвавшей их процедуры. Вы можете создать миллионы сопрограмм, но в каждый момент будет работать только одна из них. Так что сопрограммы скорее похожи на функции, чем на потоки. Но функции, приостановленные с целью продолжения.

Сопрограммы вообще требуют очень мало ресурсов. При первом вызове в куче выделяется небольшая структура (std::coroutine_handle), в которую заносятся параметры и локальные переменные сопрограммы. Этого достаточно, чтобы продолжить её выполнение. Когда сопрограмма больше не нужна, эту структуру следует освободить вызовом метода destroy().

Вот пример вызова этой сопрограммы:
int main()
{
std::coroutine_handle<> continuation = counter();
for (int i = 0; i < 5; ++i) {
std::cout << "In main function\n";
continuation.resume();
}
continuation.destroy();
}
Программа напечатает:
In main function
counter: 1
In main function
counter: 2
In main function
counter: 3
In main function
counter: 4
In main function
counter: 5
Всё это выглядит довольно просто, но самая хитрость, как вы догадываетесь - в типах 'co_void_t' и 'co_await_t'. Сейчас я вам их покажу, и тут уже просто не будет. 😀 Разработчики стандарта Си++20 несколько перемудрили. К счастью, необязательно полностью осознавать потроха этих типов, чтобы ними пользоваться.

Класс co_void_t (я сам выбрал имя, вы можете выбрать любое другое) используется как возвращаемое значение для сопрограмм, возвращающих void. При первом вызове функции сопрограммы возвращается coroutine_handle, и происходит это до того, как начнёт выполняться собственно код функции. При завершении сопрограммы (посредством return), дескриптор не удаляется автоматически, и вызывающий код должен вызвать метод destroy(). Подробности смотрите в статье Дэвида Мазиереса.
class co_void_t {
public:
struct promise_type {
co_void_t get_return_object() { return { *this }; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() {}
void return_void() noexcept {}
};

co_void_t(promise_type &promise)
: handle(std::coroutine_handle<promise_type>::from_promise(promise))
{
}

operator std::coroutine_handle<>() const { return handle; }

private:
std::coroutine_handle<promise_type> handle;
};
Класс co_await_t (опять же, имя не догма) безусловно приостанавливает сопрограмму и переключает выполнение на вызвавший код. Так мне было удобно для моих целей. Но здесь возможны варианты, и их можно изучить по статье Дэвида Мазиереса.
struct co_await_t {
constexpr bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> handle) {}
constexpr void await_resume() const noexcept {}
};
Если зафиксировать типы 'co_void_t' и 'co_await_t' как указано выше, сопрограммы превращаются в удобный механизм для реализации всяческих симуляторов. Про это я расскажу в отдельной статье.

Всё вышеуказанное замечательно работает на компиляторе g++ 10.3 под Линуксом и Mac OS. Полный исходный код примера находится здесь: demo.cpp. Компилируется он так:
g++ -std=c++20 -fcoroutines demo.cpp -o demo
То же самое можно собрать на Маке посредством последней версии родного компилятора clang++ 12.0. Но там сопрограммы пока ещё находятся в пространстве имён std::experimental, поэтому нужна другая версия исходного текста: demo-mac.cpp. Вызов компилятора:
clang++ -std=c++20 -stdlib=libc++ -fcoroutines-ts demo-mac.cpp -o demo

Date: 2021-06-21 00:45 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

Я как-то не понимаю внезапного интереса к этой древней фигне. Я ее имплементировал в 1974-м году, побаловался, употребил в продакшене, не понравилось, ну и завязал.

Date: 2021-06-21 07:49 (UTC)
vit_r: default (Default)
From: [personal profile] vit_r
Это индийский пулемёт для стрельбы в ногу.

Когда-то люди отказывались от лишних возможностей, чтобы сократить вероятность ошибок. Сейчас тренд обратный.

(no subject)

From: [personal profile] juan_gandhi - Date: 2021-06-21 12:48 (UTC) - Expand

(no subject)

From: [personal profile] vit_r - Date: 2021-06-21 13:11 (UTC) - Expand

(no subject)

From: [personal profile] juan_gandhi - Date: 2021-06-21 19:30 (UTC) - Expand

(no subject)

From: [personal profile] dmm - Date: 2021-06-28 02:02 (UTC) - Expand

(no subject)

From: [personal profile] anonim_legion - Date: 2021-06-28 04:24 (UTC) - Expand

(no subject)

From: [personal profile] vit_r - Date: 2021-06-21 19:32 (UTC) - Expand

Date: 2021-06-28 07:19 (UTC)
chaource: (Default)
From: [personal profile] chaource
У меня простое объясненiе въ два шага.

Шагъ первый. Разработчики стандарта С++ поставили себѣ цѣль держать носъ по вѣтру и добавлять въ С++ фичи, чтобы повысить "популярность" С++. Популярности больше, чѣмъ у Пайтона, ни у кого нѣтъ. Въ Пайтонъ сопрограммы были встроены гдѣ-то въ 2010-2011 годахъ.

Шагъ второй. Гдѣ-то въ 2012-2014 году хайпъ на сопрограммы среди пайтонистовъ достигъ максимума. Нѣкто въ комитетѣ стандарта С++ рѣшилъ, что пора. Сопрограммы должны быть встроены въ С++. Семь лѣтъ тяжкихъ трудовъ, и ура. Очередной индiйскiй пулеметъ для стрѣльбы въ ногу былъ добавленъ въ стандартъ С++.

Date: 2021-06-21 03:52 (UTC)
ircicq: (Default)
From: [personal profile] ircicq
После нескольких лет работы с аналогичным механизмом Tasks в C# возникло ощущение что языки (и C# и C++) не приспособлены для его удобного использования.

Постепенно все функции обрамляются магическими словами async/await (co_await в C++) и усилия уходят на их вбивание и читание.

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



Date: 2021-06-21 04:01 (UTC)
archaicos: Шарж (Default)
From: [personal profile] archaicos
Мне думается, незаметность недостижима в языках типа C/C++, где слишком легко написать ерунду и полезть куда-то, куда не нужно или не вовремя. Или думать, или язык должен быть с заборами и шлагбаумами почти везде.

Date: 2021-06-21 12:09 (UTC)
ufm: (Default)
From: [personal profile] ufm
ponylang

Date: 2021-06-21 12:25 (UTC)
From: [identity profile] cross-join.livejournal.com
Именно так, люди ставят везде магические слова, даже не утруждаясь не то, чтобы подумать, а часто просто не понимая смысл.
Edited Date: 2021-06-21 13:44 (UTC)

(no subject)

From: [personal profile] ircicq - Date: 2021-06-21 19:54 (UTC) - Expand

Date: 2021-06-27 19:59 (UTC)
From: [personal profile] sergegers
Есть реализация в boost именно таких коротин, и при разработке стандарта создатель ASIO указывал на этот недостаток. Но был выбран C# подход.

Date: 2021-06-21 08:18 (UTC)
norian: (Default)
From: [personal profile] norian
эта "небольшая структура" небольшая только в учебных примерах, туда весь стек дампицца туда-обратно плюс обёртка

имко лучше рэзат методы класса на куски и инлайнить

вообще смысл функциональной симуляции в выполнении как можно ближе к рантайму, лучше на какой-нть кластер раскидать чем в один поток всё

бтв, а хто-нть встречал бесстековые треды в жывой природе ?
Edited Date: 2021-06-21 09:02 (UTC)

(no subject)

From: [personal profile] norian - Date: 2021-06-21 20:05 (UTC) - Expand

(no subject)

From: [personal profile] norian - Date: 2021-06-22 07:21 (UTC) - Expand

Date: 2021-06-28 08:26 (UTC)
elglin: (Default)
From: [personal profile] elglin
Если про бесстековые корутины, то я встречал. 2M rps, Boost::asio.

Date: 2021-06-21 08:21 (UTC)
dluciv: (Default)
From: [personal profile] dluciv

Разработчики стандарта Си++20 несколько перемудрили.

Никогда такого не было!

Date: 2021-06-21 14:03 (UTC)
From: [identity profile] cross-join.livejournal.com
Из примера не совсем ясно, зачем сопрограммы нужны. В Сишарпе типовое использование: вызвать кучу асинхронных функций, доставляющих данные на веб-смтраничку и потом ждать, пока все закончатся.

Date: 2021-06-21 18:04 (UTC)
From: [personal profile] ivanrubilo
Вот, кстати, да. Примерно понятно, но не очевидно. Какой-то конкретный пример было бы хорошо, а то мне не особо понятно чем это (да и континюэйшены) лучше тредов (ну кроме ресурсов и вообще машинерии в ОС).
А вот ещё было популярно интерпретаторы Бейсика делать на «шитом» коде - это из той же оперы?

(no subject)

From: [personal profile] ircicq - Date: 2021-06-21 19:51 (UTC) - Expand

Date: 2021-06-22 13:17 (UTC)
From: [personal profile] ex0_planet
Классический пример это producer/consumer. Без корутин их оба невозможно написать в линейном стиле, кто-то из двух обязан быть стейт-машиной, ну или еще более извратные (для императивных языков) конструкции нужны будут вроде CPS.

(no subject)

From: [identity profile] cross-join.livejournal.com - Date: 2021-06-22 13:43 (UTC) - Expand

(no subject)

From: [personal profile] ex0_planet - Date: 2021-06-22 13:49 (UTC) - Expand

(no subject)

From: [identity profile] cross-join.livejournal.com - Date: 2021-06-22 13:53 (UTC) - Expand

(no subject)

From: [personal profile] ex0_planet - Date: 2021-06-22 13:57 (UTC) - Expand

(no subject)

From: [identity profile] cross-join.livejournal.com - Date: 2021-06-22 14:03 (UTC) - Expand

(no subject)

From: [personal profile] ex0_planet - Date: 2021-06-22 14:07 (UTC) - Expand

(no subject)

From: [identity profile] cross-join.livejournal.com - Date: 2021-06-22 14:21 (UTC) - Expand

(no subject)

From: [personal profile] ex0_planet - Date: 2021-06-22 14:38 (UTC) - Expand

(no subject)

From: [identity profile] cross-join.livejournal.com - Date: 2021-06-22 15:17 (UTC) - Expand

(no subject)

From: [personal profile] ex0_planet - Date: 2021-06-22 15:22 (UTC) - Expand

(no subject)

From: [identity profile] cross-join.livejournal.com - Date: 2021-06-22 15:39 (UTC) - Expand

(no subject)

From: [identity profile] cross-join.livejournal.com - Date: 2021-06-22 18:07 (UTC) - Expand

(no subject)

From: [identity profile] cross-join.livejournal.com - Date: 2021-06-23 09:56 (UTC) - Expand

Date: 2021-06-28 06:23 (UTC)
From: [personal profile] qse

на винде тоже здорово работает c флагом /await

Date: 2021-06-30 23:36 (UTC)
From: [identity profile] johnconst.livejournal.com
Хорошее чтиво.
https://lewissbaker.github.io/

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

(no subject)

From: [identity profile] johnconst.livejournal.com - Date: 2021-07-01 00:11 (UTC) - Expand

Date: 2021-07-01 00:15 (UTC)
From: [identity profile] johnconst.livejournal.com
Вообще, если нужна работа с задачами (Task) в С++, есть очень классная библиотека, которая лично мне очень нравится в плане реализации и скорости работы.

https://taskflow.github.io/
https://github.com/taskflow/taskflow

nz

Date: 2022-02-22 23:29 (UTC)
From: [personal profile] nz

Не совсем понятно, какой в этом всем профит.

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

Да, это подается как "можно писать асинхронный код, как синхронный", но у меня, например, от этих реентерабельных функций, исполняющихся не с начала, а откуда-то из середины цикла, дергается глаз. Explicit is better than implicit.