Сопрограммы (C++20)
Сопрограмма это функция, которая может приостановить выполнение, чтобы возобновить его позже. Сопрограммы не имеют стека: они приостанавливают выполнение, возвращаясь к вызывающей стороне, а данные, необходимые для возобновления выполнения, хранятся отдельно от стека. Это позволяет использовать последовательный код, который выполняется асинхронно (например, для обработки неблокирующего Ввода/Вывода без явных обратных вызовов), а также поддерживает алгоритмы бесконечных последовательностей с отложенным вычислением и другие применения.
Функция является сопрограммой, если её определение содержит одно из следующего:
- использует выражение co_await для приостановки выполнения до возобновления
task<> tcp_echo_server(){char data[1024];while(true){ size_t n = co_await socket.async_read_some(buffer(data)); co_await async_write(socket, buffer(data, n));}}
- использует выражение co_yield, чтобы приостановить выполнение, возвращая значение
generator<unsignedint> iota(unsignedint n =0)while(true) co_yield n++;}
- использует оператор co_return для завершения выполнения, возвращая значение
lazy<int> f(){ co_return 7;}
Каждая сопрограмма должна иметь возвращаемый тип, который соответствует ряду требований, указанных ниже.
Содержание |
[править]Ограничения
Сопрограммы не могут использовать вариативные аргументы, простые операторы return или возвращаемые типы заполнители (auto
или Концепт
).
Функции constexpr, конструкторы, деструкторы и функция main не могут быть сопрограммами.
[править]Выполнение
Каждая сопрограмма связана с
- обещающий объект, управляемый изнутри сопрограммы. Сопрограмма отправляет свой результат или исключение через этот объект.
- дескриптор сопрограммы, управляемый из-за пределов сопрограммы. Это дескриптор, не являющийся владельцем, используемый для возобновления выполнения сопрограммы или для уничтожения фрейма сопрограммы.
- состояние сопрограммы, которое является внутренней, динамически распределемой памятью (если выделение не оптимизировано), объектом, который содержит
- обещающий объект
- параметры (все копируются по значению)
- некоторое представление текущей точки приостановки, так что восстановление знает, где продолжить, а уничтожение знает, какие локальные переменные были в области видимости
- локальные переменные и временные объекты, время жизни которых охватывает текущую точку приостановки
Когда сопрограмма начинает выполнение, она выполняет следующее:
- распределяет объект состояния сопрограммы, используя operator new (смотрите ниже)
- копирует все параметры функции в состояние сопрограммы: параметры по значению перемещаются или копируются, параметры по ссылке остаются ссылками (и поэтому могут стать зависшими, если сопрограмма возобновляется после окончания времени жизни упомянутого объекта — смотрите ниже примеры)
- вызывает конструктор объекта обещания. Если у типа обещания есть конструктор, который принимает все параметры сопрограммы, вызывается этот конструктор с аргументами сопрограммы после копирования. Иначе вызывается конструктор по умолчанию.
- вызывает promise.get_return_object() и сохраняет результат в локальной переменной. Результат этого вызова будет возвращен вызывающей стороне при первой приостановке сопрограммы. Любые исключения, созданные до этого шага включительно, распространяются обратно вызывающему объекту, а не помещаются в обещание.
- вызывает promise.initial_suspend() и
co_await
-ы возвращают результат. Типичные типыPromise
возвращают либо std::suspend_always, для лениво запускаемых сопрограмм, либо std::suspend_never, для нетерпеливо запускаемых сопрограмм. - когда co_await promise.initial_suspend() возобновляется, начинается выполнение тела сопрограммы.
Некоторые примеры того, как параметр становится зависшим:
#include <coroutine>#include <iostream> struct promise;struct coroutine :std::coroutine_handle<promise>{using promise_type =::promise;}; struct promise { coroutine get_return_object(){return{coroutine::from_promise(*this)};}std::suspend_always initial_suspend()noexcept{return{};}std::suspend_always final_suspend()noexcept{return{};}void return_void(){}void unhandled_exception(){}}; struct S {int i; coroutine f(){std::cout<< i; co_return;}}; void bad1(){ coroutine h = S{0}.f();// S{0} уничтожен h.resume();// возобновлённая сопрограмма выполняет std::cout << i, используя S::i// после освобождения h.destroy();} coroutine bad2(){ S s{0};return s.f();// возвращённая сопрограмма не может быть возобновлена без фиксации// использования после освобождения} void bad3(){ coroutine h =[i =0]()-> coroutine // лямбда, которая также является сопрограммой{std::cout<< i; co_return;}();// вызывается немедленно// лямбда уничтожена h.resume();// использует (анонимный тип лямбда)::i после освобождения h.destroy();} void good(){ coroutine h =[](int i)-> coroutine // делает i параметром сопрограммы{std::cout<< i; co_return;}(0);// лямбда уничтожена h.resume();// нет проблем, i скопирована во фрейм сопрограммы как параметр по значению h.destroy();}
Когда сопрограмма достигает точки приостановки
- возвращаемый объект, полученный ранее, возвращается вызывающему/возобновляющему, после неявного преобразования в возвращаемый тип сопрограммы, если это необходимо.
Когда сопрограмма достигает оператора co_return, она выполняет следующее:
- вызывает promise.return_void() для
- co_return;
- co_return выражение;, где выражение имеет тип void
- или вызывает promise.return_value(выражение) для co_return выражение, где выражение не имеет тип void
- уничтожает все переменные с автоматической длительностью хранения в порядке, обратном их созданию.
- вызывает promise.final_suspend() и co_await возвращает результат.
Падение в конце сопрограммы эквивалентно co_return;, за исключением того, что поведение не определено, если тип Promise
не имеет функции элемента Promise::return_void(). Функция, у которой нет ни одного определяющего ключевого слова в теле функции, не является сопрограммой, независимо от типа её возвращаемого значения, и падение в конце приводит к неопределённому поведению, если тип возвращаемого значения не cvvoid.
// предполагаем, что эта задача представляет собой некоторый тип задачи сопрограммы task<void> f(){// не сопрограмма, неопределённое поведение} task<void> g(){ co_return;// OK} task<void> h(){ co_await g();// OK, неявный co_return;}
Если сопрограмма завершается необработанным исключением, она выполняет следующее:
- перехватывает исключение и вызывает promise.unhandled_exception() из блока catch
- вызывает promise.final_suspend() и co_await возвращает результат (например, чтобы возобновить продолжение или опубликовать результат). Возобновление сопрограммы с этого момента является неопределённым поведением.
Когда состояние сопрограммы уничтожается либо из-за того, что она завершилась через {{c/core|co_return} или неперехваченное исключение, либо из-за того, что она была уничтожена через свой дескриптор, она делает следующее:
- вызывает деструктор объекта обещания.
- вызывает деструкторы копий параметров функции.
- вызывает operator delete, чтобы освободить память, используемую состоянием сопрограммы
- передает выполнение обратно вызывающему/возобновляющему.
[править]Динамическое размещение
Состояние сопрограммы выделяется динамически через operator new не для массивов.
Если тип Promise
определяет замену на уровне класса, она будет использоваться, в противном случае будет использоваться глобальный operator new.
Если тип Promise
определяет форму размещения operator new, которая принимает дополнительные параметры, и они соответствуют списку аргументов, где первым аргументом является запрошенный размер (типа std ::size_t), а остальные являются аргументами функции сопрограммы, эти аргументы будут переданы в operator new (это позволяет использовать для сопрограмм соглашение об ведущем аллокатор)
Вызов operator new можно оптимизировать (даже если используется пользовательский распределитель), если
- Время жизни состояния сопрограммы строго вложено в время жизни вызывающей стороны, и
- размер фрейма сопрограммы известен в месте вызова
в этом случае состояние сопрограммы встроено в кадр стека вызывающего объекта (если вызывающий объект является обычной функцией) или в состояние сопрограммы (если вызывающий объект является сопрограммой)
Если распределение не удаётся, сопрограмма выдает std::bad_alloc, если только тип Promise не определяет функцию-элемент Promise::get_return_object_on_allocation_failure(). Если эта функция-элемент определена, выделение использует форму operator new не бросающую исключение, а при сбое распределения сопрограмма немедленно возвращает объект, полученный из Promise::get_return_object_on_allocation_failure() вызывающей стороне, например:
struct Coroutine::promise_type{/* ... */ // обеспечить использование оператора new// не генерирующего исключенияstatic Coroutine get_return_object_on_allocation_failure(){std::cerr<< __func__ <<'\n';throwstd::bad_alloc();// или, возврат Coroutine(nullptr);} // пользовательская перегрузка new// не генерирующего исключенияvoid*operator new(std::size_t n)noexcept{if(void* mem =std::malloc(n))return mem;return nullptr;// сбой распределения памяти}};
[править]Promise
Тип Promise
определяется компилятором из возвращаемого типа сопрограммы используя std::coroutine_traits.
Формально пусть R
и Args...
обозначают тип возвращаемого значения и список типов параметров сопрограммы соответственно, ClassT
и /*cv-квалификация*/ (если есть) обозначают тип класса, к которому принадлежит сопрограмма, и её cv-квалификацию соответственно, если она определена как нестатическая функция-элемент, её тип Promise
определяется следующим образом:
- std::coroutine_traits<R, Args...>::promise_type, если сопрограмма не определена как нестатическая функция-элемент,
- std::coroutine_traits<R, ClassT /*cv-квалификация*/&, Args...>::promise_type, если сопрограмма определена как нестатическая функция-элемент, которая не является квалифицированной ссылкой на rvalue,
- std::coroutine_traits<R, ClassT /*cv-квалификация*/&&, Args...>::promise_type, если сопрограмма определена как нестатическая функция-элемент, которая является квалифицированной ссылкой на rvalue.
Например:
Если сопрограмма определена как ... | тогда её тип Promise равен ... |
---|---|
task<void> foo(int x); | std::coroutine_traits<task<void>, int>::promise_type |
task<void> Bar::foo(int x)const; | std::coroutine_traits<task<void>, const Bar&, int>::promise_type |
task<void> Bar::foo(int x)&&; | std::coroutine_traits<task<void>, Bar&&, int>::promise_type |
[править]co_await
Унарный оператор co_await приостанавливает сопрограмму и возвращает управление вызывающей стороне. Его операнд представляет собой выражение, которое в случае (1) имеет классовый тип, который определяет элемент operator co_await, либо может быть передан неэлементу operator co_await, или в случае (2) можно преобразовать в такой классовый тип с помощью Promise::await_transform текущей сопрограммы.
co_await выражение | |||||||||
Выражение co_await может появляться только в потенциально оцениваемом выражении в теле обычной функции и не может появляться
- в обработчике исключений,
- в объявлении, если только она не появляется в инициализаторе этой инструкции объявления,
- в нет названия разделаоператора-инициализации (смотрите
if
,switch
,for
и range-for), если только оно не появляется в инициализаторе этого оператора-инициализации, - в ргументе по умолчанию, или
- в инициализаторе переменной области видимости блока со статической или потоковой длительностью хранения.
Во-первых, выражение преобразуется в ожидаемое следующим образом:
- если выражение создаётся начальной точкой приостановки, конечной точкой приостановки или выражением yield, ожидаемым является выражение, как есть.
- иначе, если тип
Promise
текущей сопрограммы имеет функцию-элементawait_transform
, то ожидаемое значение равно promise.await_transform(выражение) - иначе ожидаемым является выражение, как есть.
Затем объект ожидания получается следующим образом:
- если разрешение перегрузки для operator co_await даёт единственную наилучшую перегрузку, ожидающий является результатом этого вызова:
- awaitable.operator co_await() для перегрузки элемента,
- operator co_await(static_cast<Awaitable&&>(awaitable)) для перегрузки, не являющейся элементом.
- иначе, если разрешение перегрузки не находит оператор co_await, ожидающий является ожидаемым, как есть.
- иначе, если разрешение перегрузки неоднозначно, программа некорректна
Если приведённое выше выражение является значением prvalue, объект ожидания является временно материализованным из него. Иначе, если приведённое выше выражение является значением glvalue, объектом ожидания является объект, на который оно ссылается.
Затем вызывается awaiter.await_ready() (это короткий путь, позволяющий избежать затрат на приостановку, если известно, что результат готов или может быть выполнен синхронно). Если её результат, контекстно преобразованный в bool, равен false, тогда
- Сопрограмма приостанавливается (её состояние сопрограммы заполняется локальными переменными и текущей точкой приостановки).
- Вызывается awaiter.await_suspend(handle), где handle это дескриптор сопрограммы, представляющий текущую сопрограмму. Внутри этой функции состояние приостановленной сопрограммы можно наблюдать через этот дескриптор, и эта функция несёт ответственность за планирование её возобновления на каком-либо исполнителе или её уничтожение (возвращение ложных счётчиков в качестве планирования)
- если await_suspend возвращает void, управление немедленно возвращается вызывающей/возобновляющей текущей сопрограмме (эта сопрограмма остаётся приостановленной), иначе
- если await_suspend возвращает логическое значение,
- значение true возвращает управление вызывающей стороне/возобновителю текущей сопрограммы
- значение false возобновляет текущую сопрограмму.
- если await_suspend возвращает дескриптор сопрограммы для какой-либо другой сопрограммы, этот дескриптор возобновляется (вызовом handle.resume()) (обратите внимание, что это может привести к цепочке, которая в конечном итоге приведёт к возобновлению текущей сопрограммы)
- если await_suspend выбрасывает исключение, исключение перехватывается, сопрограмма возобновляется, и исключение немедленно выбрасывается повторно
Наконец, вызывается awaiter.await_resume() (независимо от того, была ли сопрограмма приостановлена или нет), и её результатом является результат всего выражения co_await выражение.
Если сопрограмма была приостановлена в выражении co_await и позже возобновлена, точка возобновления находится непосредственно перед вызовом awaiter.await_resume().
Обратите внимание, поскольку сопрограмма полностью приостанавливается перед входом в awaiter.await_suspend(), эта функция может свободно передавать дескриптор сопрограммы между потоками без дополнительной синхронизации. Например, она может поместить его в обратный вызов, запланированный для запуска в пуле потоков после завершения операции асинхронного Ввода/Вывода. В этом случае, поскольку текущая сопрограмма могла быть возобновлена и, таким образом, выполнила деструктор объекта ожидания, всё одновременно, поскольку await_suspend()
продолжает свое выполнение в текущем потоке, await_suspend()
должен рассматривать *this как уничтоженный и не обращаться к нему после того, как дескриптор был опубликован в других потоках.
[править]Пример
#include <coroutine>#include <iostream>#include <stdexcept>#include <thread> auto switch_to_new_thread(std::jthread& out){struct awaitable {std::jthread* p_out;bool await_ready(){returnfalse;}void await_suspend(std::coroutine_handle<> h){std::jthread& out =*p_out;if(out.joinable())throwstd::runtime_error("Выходной параметр jthread не пустой"); out =std::jthread([h]{ h.resume();});// Возможно неопределённое поведение: доступ к потенциально уничтоженному *this// std::cout << "Идентификатор нового потока: " << p_out->get_id() << '\n';std::cout<<"Идентификатор нового потока: "<< out.get_id()<<'\n';// это OK}void await_resume(){}};return awaitable{&out};} struct task {struct promise_type { task get_return_object(){return{};}std::suspend_never initial_suspend(){return{};}std::suspend_never final_suspend()noexcept{return{};}void return_void(){}void unhandled_exception(){}};}; task resuming_on_new_thread(std::jthread& out){std::cout<<"Сопрограмма запущена в потоке: "<<std::this_thread::get_id()<<'\n'; co_await switch_to_new_thread(out);// ожидающий уничтожен здесьstd::cout<<"Сопрограмма возобновлена в потоке: "<<std::this_thread::get_id()<<'\n';} int main(){std::jthread out; resuming_on_new_thread(out);}
Возможный вывод:
Сопрограмма запущена в потоке: 139972277602112 Идентификатор нового потока: 139972267284224 Сопрограмма возобновлена в потоке: 139972267284224
Примечание: объект ожидания является частью состояния сопрограммы (как временное состояние, время жизни которого пересекает точку приостановки) и уничтожается до завершения выражения co_await. Его можно использовать для поддержания состояния каждой операции в соответствии с требованиями некоторых API-интерфейсов асинхронного Ввода/Вывода, не прибегая к дополнительному динамическому выделению.
Стандартная библиотека определяет два тривиальных объекта ожидания: std::suspend_always и std::suspend_never.
Этот раздел не завершён Причина: примеры |
[править]co_yield
Выражение co_yield возвращает значение вызывающей стороне и приостанавливает текущую сопрограмму: это общий строительный блок возобновляемых функций генератора
co_yield выражение | |||||||||
co_yield список-инициализации-в-фигурных-скобках | |||||||||
Это эквивалентно
co_await promise.yield_value(выражение)
yield_value
типичного генератора будет хранить (копировать/перемещать или просто хранить адрес, поскольку время жизни аргумента пересекает точку приостановки внутри co_await) свой аргумент в объекте генератора и возвращать std::suspend_always, передавая управление вызывающему/возобновляющему.
#include <coroutine>#include <exception>#include <iostream> template<typename T>struct Generator {// Мы выбрали имя класса 'Generator', и оно не требуется для магии сопрограмм.// Компилятор распознаёт сопрограмму по наличию ключевого слова 'co_yield'.// Вместо этого вы можете использовать имя 'MyGenerator' (или любое другое),// если вы включите вложенную структуру promise_type с методом// 'MyGenerator get_return_object()'.// Примечание. Вам также необходимо настроить имена конструктора/деструктора// класса при переименования класса. struct promise_type;using handle_type =std::coroutine_handle<promise_type>; struct promise_type // требуется{ T value_;std::exception_ptr exception_; Generator get_return_object(){return Generator(handle_type::from_promise(*this));}std::suspend_always initial_suspend(){return{};}std::suspend_always final_suspend()noexcept{return{};}void unhandled_exception(){ exception_ =std::current_exception();}// сохранение// исключения template<std::convertible_to<T> From>// концепт C++20std::suspend_always yield_value(From &&from){ value_ =std::forward<From>(from);// кэширование результата в обещанииreturn{};}void return_void(){}}; handle_type h_; Generator(handle_type h): h_(h){} ~Generator(){ h_.destroy();}explicit operator bool(){ fill();// Единственный способ надежно узнать, закончили ли мы сопрограмму или нет,// будет ли сгенерировано следующее значение (co_yield) в сопрограмме с// помощью геттера C++ (operator () ниже) это выполнить/возобновить сопрограмму// до следующей co_yield точки (или пусть она упадёт с конца).// Затем мы сохраняем/кешируем результат в обещании, чтобы позволить геттеру// (operator() ниже получить его без выполнения сопрограммы).return!h_.done();} T operator()(){ fill(); full_ =false;// мы собираемся переместить ранее кэшированный результат,// чтобы снова сделать обещание пустымreturn std::move(h_.promise().value_);} private:bool full_ =false; void fill(){if(!full_){ h_();if(h_.promise().exception_)std::rethrow_exception(h_.promise().exception_);// распространить исключение сопрограммы в вызываемом контексте full_ =true;}}}; Generator<uint64_t> fibonacci_sequence(unsigned n){ if(n==0) co_return; if(n>94)throwstd::runtime_error("Слишком большая последовательность Фибоначчи. ""Элементы будут переполнены."); co_yield 0; if(n==1) co_return; co_yield 1; if(n==2) co_return; uint64_t a=0; uint64_t b=1; for(unsigned i =2; i < n; i++){ uint64_t s=a+b; co_yield s; a=b; b=s;}} int main(){try{ auto gen = fibonacci_sequence(10);// максимум 94 до переполнения uint64_t for(int j=0; gen; j++)std::cout<<"fib("<<j <<")="<< gen()<<'\n'; }catch(conststd::exception& ex){std::cerr<<"Исключение: "<< ex.what()<<'\n';}catch(...){std::cerr<<"Неизвестное исключение.\n";}}
Вывод:
fib(0)=0 fib(1)=1 fib(2)=1 fib(3)=2 fib(4)=3 fib(5)=5 fib(6)=8 fib(7)=13 fib(8)=21 fib(9)=34
[править]Примечание
Макрос тест функциональности | Значение | Стандарт | Комментарий |
---|---|---|---|
__cpp_impl_coroutine | 201902L | (C++20) | Сопрограммы (поддержка компилятора) |
__cpp_lib_coroutine | 201902L | (C++20) | Сопрограммы (поддержка библиотеки) |
__cpp_lib_generator | 202207L | (C++23) | std::generator: синхронный генератор сопрограмм для диапазонов |
[править]Библиотеки поддержки
Библиотека поддержки сопрограмм определяет несколько типов, обеспечивающих поддержку сопрограмм во время компиляции и выполнения.
[править]Смотрите также
(C++23) | view , представляющий синхронный генератор сопрограмм(шаблон класса) |
[править] Внешние ссылки
1. | David Mazières, 2021 - Учебник по сопрограммам C++20. |
2. | Lewis Baker, 2017-2022 - Асимметричный Перенос. |