![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
В последнем стандарте языка Си++ 2020 появилась поддержка сопрограмм (coroutines). Я давно к ним присматриваюсь, в надежде применить для симуляции цифровой логики. При очередной попытке понять сопрограммы я натолкнулся на статью Дэвида Мазиереса, и она столкнула меня с мёртвой точки. Попробую ещё больше упростить здесь для начинающих.
Сопрограммы в языке Си++ это такие процедуры или функции, которые не завершаются до конца, и их можно продолжить. Выглядит сопрограмма как обычная функция, но с двумя отличиями:
Оператор 'co_await' в теле сопрограммы приостанавливает её и возвращает управление в вызвавший (или продолживший) её код. Также могут встречаться операторы 'co_yield' и 'co_return'.
Вот пример простой сопрограммы, при каждом продолжении печатающей нарастающий счётчик:
Сопрограммы вообще требуют очень мало ресурсов. При первом вызове в куче выделяется небольшая структура (std::coroutine_handle), в которую заносятся параметры и локальные переменные сопрограммы. Этого достаточно, чтобы продолжить её выполнение. Когда сопрограмма больше не нужна, эту структуру следует освободить вызовом метода destroy().
Вот пример вызова этой сопрограммы:
Класс co_void_t (я сам выбрал имя, вы можете выбрать любое другое) используется как возвращаемое значение для сопрограмм, возвращающих void. При первом вызове функции сопрограммы возвращается coroutine_handle, и происходит это до того, как начнёт выполняться собственно код функции. При завершении сопрограммы (посредством return), дескриптор не удаляется автоматически, и вызывающий код должен вызвать метод destroy(). Подробности смотрите в статье Дэвида Мазиереса.
Всё вышеуказанное замечательно работает на компиляторе g++ 10.3 под Линуксом и Mac OS. Полный исходный код примера находится здесь: demo.cpp. Компилируется он так:
Сопрограммы в языке Си++ это такие процедуры или функции, которые не завершаются до конца, и их можно продолжить. Выглядит сопрограмма как обычная функция, но с двумя отличиями:
- У неё особый тип возвращаемого значения.
- В её теле встречается оператор 'co_await'. Аргумент этого оператора тоже имеет специальный тип.
Оператор 'co_await' в теле сопрограммы приостанавливает её и возвращает управление в вызвавший (или продолживший) её код. Также могут встречаться операторы 'co_yield' и 'co_return'.
Вот пример простой сопрограммы, при каждом продолжении печатающей нарастающий счётчик:
Вероятно, вам приходилось иметь дело с потоками std::thread или std::jthread. Сопрограммы - это не потоки. В отличие от потоков, они не используют никаких ресурсов операционной системы (наподобие pthreads). Также не стоит путать сопрограммы с fibers, или green threads, или традиционных вызовов setjmp()/longjmp(). В отличие от fibers, сопрограммы не имеют собственного стека. Они выполняются на стеке вызвавшей их процедуры. Вы можете создать миллионы сопрограмм, но в каждый момент будет работать только одна из них. Так что сопрограммы скорее похожи на функции, чем на потоки. Но функции, приостановленные с целью продолжения.co_void_t counter()
{
for (int i = 1; ; ++i) {
std::cout << "counter: " << i << std::endl;
co_await co_await_t{};
}
}
Сопрограммы вообще требуют очень мало ресурсов. При первом вызове в куче выделяется небольшая структура (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();
}
Всё это выглядит довольно просто, но самая хитрость, как вы догадываетесь - в типах 'co_void_t' и 'co_await_t'. Сейчас я вам их покажу, и тут уже просто не будет. 😀 Разработчики стандарта Си++20 несколько перемудрили. К счастью, необязательно полностью осознавать потроха этих типов, чтобы ними пользоваться.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 (я сам выбрал имя, вы можете выбрать любое другое) используется как возвращаемое значение для сопрограмм, возвращающих void. При первом вызове функции сопрограммы возвращается coroutine_handle, и происходит это до того, как начнёт выполняться собственно код функции. При завершении сопрограммы (посредством return), дескриптор не удаляется автоматически, и вызывающий код должен вызвать метод destroy(). Подробности смотрите в статье Дэвида Мазиереса.
Класс co_await_t (опять же, имя не догма) безусловно приостанавливает сопрограмму и переключает выполнение на вызвавший код. Так мне было удобно для моих целей. Но здесь возможны варианты, и их можно изучить по статье Дэвида Мазиереса.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_void_t' и '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 {}
};
Всё вышеуказанное замечательно работает на компиляторе g++ 10.3 под Линуксом и Mac OS. Полный исходный код примера находится здесь: demo.cpp. Компилируется он так:
То же самое можно собрать на Маке посредством последней версии родного компилятора clang++ 12.0. Но там сопрограммы пока ещё находятся в пространстве имён std::experimental, поэтому нужна другая версия исходного текста: demo-mac.cpp. Вызов компилятора:g++ -std=c++20 -fcoroutines demo.cpp -o demo
clang++ -std=c++20 -stdlib=libc++ -fcoroutines-ts demo-mac.cpp -o demo
no subject
Date: 2021-06-21 00:45 (UTC)Я как-то не понимаю внезапного интереса к этой древней фигне. Я ее имплементировал в 1974-м году, побаловался, употребил в продакшене, не понравилось, ну и завязал.
no subject
Date: 2021-06-21 00:48 (UTC)no subject
Date: 2021-06-21 07:49 (UTC)Когда-то люди отказывались от лишних возможностей, чтобы сократить вероятность ошибок. Сейчас тренд обратный.
(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:no subject
Date: 2021-06-28 07:19 (UTC)Шагъ первый. Разработчики стандарта С++ поставили себѣ цѣль держать носъ по вѣтру и добавлять въ С++ фичи, чтобы повысить "популярность" С++. Популярности больше, чѣмъ у Пайтона, ни у кого нѣтъ. Въ Пайтонъ сопрограммы были встроены гдѣ-то въ 2010-2011 годахъ.
Шагъ второй. Гдѣ-то въ 2012-2014 году хайпъ на сопрограммы среди пайтонистовъ достигъ максимума. Нѣкто въ комитетѣ стандарта С++ рѣшилъ, что пора. Сопрограммы должны быть встроены въ С++. Семь лѣтъ тяжкихъ трудовъ, и ура. Очередной индiйскiй пулеметъ для стрѣльбы въ ногу былъ добавленъ въ стандартъ С++.
no subject
Date: 2021-06-21 03:52 (UTC)Постепенно все функции обрамляются магическими словами async/await (co_await в C++) и усилия уходят на их вбивание и читание.
Должен быть язык, который реализует асинхронщину незаметно для программиста.
no subject
Date: 2021-06-21 04:01 (UTC)no subject
Date: 2021-06-21 12:09 (UTC)no subject
Date: 2021-06-21 12:25 (UTC)no subject
Date: 2021-06-21 19:06 (UTC)(no subject)
From:no subject
Date: 2021-06-27 19:59 (UTC)no subject
Date: 2021-06-21 08:18 (UTC)имко лучше рэзат методы класса на куски и инлайнить
вообще смысл функциональной симуляции в выполнении как можно ближе к рантайму, лучше на какой-нть кластер раскидать чем в один поток всё
бтв, а хто-нть встречал бесстековые треды в жывой природе ?
no subject
Date: 2021-06-21 19:08 (UTC)(no subject)
From:(no subject)
From:(no subject)
From:no subject
Date: 2021-06-28 08:26 (UTC)no subject
Date: 2021-06-21 08:21 (UTC)Никогда такого не было!
no subject
Date: 2021-06-21 22:46 (UTC)no subject
Date: 2021-06-21 14:03 (UTC)no subject
Date: 2021-06-21 18:04 (UTC)А вот ещё было популярно интерпретаторы Бейсика делать на «шитом» коде - это из той же оперы?
(no subject)
From:(no subject)
From:no subject
Date: 2021-06-21 19:25 (UTC)no subject
Date: 2021-06-22 13:17 (UTC)(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:no subject
Date: 2021-06-28 06:23 (UTC)на винде тоже здорово работает c флагом /await
no subject
Date: 2021-06-28 07:06 (UTC)no subject
Date: 2021-06-30 23:36 (UTC)https://lewissbaker.github.io/
Тому, кто работал с RTOS, корутины напоминают Task (выполняемую задачу). Только у корутин не вытесняющий планировщик, а кооперативный, а также нету стека и кое-каких других данных, которые есть у задачи в RTOS
no subject
Date: 2021-07-01 00:05 (UTC)(no subject)
From:(no subject)
From:no subject
Date: 2021-07-01 00:15 (UTC)https://taskflow.github.io/
https://github.com/taskflow/taskflow
no subject
Date: 2021-07-01 00:25 (UTC)nz
Date: 2022-02-22 23:29 (UTC)Не совсем понятно, какой в этом всем профит.
Аналогичный эффект можно получить, сделав stateful functor, запоминающий свое состояние при выходе и восстанавливающий при следующем вызове, причем без всякой закулисной магии со стеком.
Да, это подается как "можно писать асинхронный код, как синхронный", но у меня, например, от этих реентерабельных функций, исполняющихся не с начала, а откуда-то из середины цикла, дергается глаз. Explicit is better than implicit.
Re: nz
Date: 2022-02-22 23:54 (UTC)