Корутины (C++20)
Корутина — это функция, выполнение которой может быть приостановлено и возобновлено позднее. Корутины не используют стек: они приостанавливают выполнение, возвращаясь к вызывающей функции, а данные, необходимые для возобновления выполнения, хранятся отдельно от стека. Это позволяет писать последовательный код, выполняющийся асинхронно (например, для обработки неблокирующего ввода-вывода без явных обратных вызовов), а также поддерживает алгоритмы для бесконечных последовательностей, вычисляемых по требованию, и другие сценарии использования.
Функция является корутиной, если её определение содержит что-либо из следующего:
- выражение co_await — для приостановки выполнения до его возобновления
task<> tcp_echo_server()
{
char data[1024];
while (true)
{
std::size_t n = co_await socket.async_read_some(buffer(data));
co_await async_write(socket, buffer(data, n));
}
}
- выражение co_yield — для приостановки выполнения с возвратом значения
generator<unsigned int> iota(unsigned int n = 0)
{
while (true)
co_yield n++;
}
- оператор co_return — для завершения выполнения с возвратом значения
lazy<int> f()
{
co_return 7;
}
Каждая корутина должна иметь тип возвращаемого значения, удовлетворяющий ряду требований, описанных ниже.
Ограничения
В корутинах нельзя использовать аргументы переменного числа, обычные операторы return или типы возвращаемого значения-заполнители (auto или концепт).
Функции consteval, функции constexpr, конструкторы, деструкторы и функция main не могут быть корутинами.
Выполнение
С каждой корутиной связаны
- объект обещания, которым управляют изнутри корутины. Корутина передаёт через этот объект результат или исключение. Объекты обещания никак не связаны с
std::promise. - дескриптор корутины, которым управляют извне корутины. Это невладеющий дескриптор, используемый для возобновления выполнения корутины или уничтожения её состояния.
- состояние корутины — внутренний объект с динамически выделяемой памятью (если только выделение не оптимизировано), содержащий
- объект обещания
- параметры (все копируются по значению)
- некоторое представление текущей точки приостановки, чтобы при возобновлении было известно, с какого места продолжить, а при уничтожении — какие локальные переменные находились в области видимости
- локальные переменные и временные объекты, время жизни которых охватывает текущую точку приостановки.
При начале выполнения корутина выполняет следующие действия:
- выделяет память для объекта состояния корутины с помощью
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} destroyed
h.resume(); // resumed coroutine executes std::cout << i, uses S::i after free
h.destroy();
}
coroutine bad2()
{
S s{0};
return s.f(); // returned coroutine can't be resumed without committing use after free
}
void bad3()
{
coroutine h = [i = 0]() -> coroutine // a lambda that's also a coroutine
{
std::cout << i;
co_return;
}(); // immediately invoked
// lambda destroyed
h.resume(); // uses (anonymous lambda type)::i after free
h.destroy();
}
void good()
{
coroutine h = [](int i) -> coroutine // make i a coroutine parameter
{
std::cout << i;
co_return;
}(0);
// lambda destroyed
h.resume(); // no problem, i has been copied to the coroutine
// state as a by-value parameter
h.destroy();
}
Когда корутина достигает точки приостановки
- объект возвращаемого значения, полученный ранее, возвращается вызывающей функции или функции, возобновившей выполнение, после неявного преобразования к типу возвращаемого значения корутины, если это необходимо.
При достижении оператором корутины co_return выполняются следующие действия:
- вызывает
promise.return_void()для co_return;-
co_return expr;, гдеexprимеет тип void - или вызывает
promise.return_value(expr)дляco_return expr;, гдеexprимеет тип, отличный от void - уничтожает все переменные с автоматическим временем хранения в порядке, обратном порядку их создания.
- вызывает
promise.final_suspend()и применяет co_await к результату.
Выход за конец корутины эквивалентен co_return;, за исключением того, что поведение не определено, если в области видимости Promise не найдено объявлений return_void. Функция, в теле которой нет ни одного из ключевых слов, определяющих корутину, не является корутиной независимо от типа её возвращаемого значения; выход за конец такой функции приводит к неопределённому поведению, если тип возвращаемого значения не является (возможно, cv-квалифицированным) void.
// assuming that task is some coroutine task type
task<void> f()
{
// not a coroutine, undefined behavior
}
task<void> g()
{
co_return; // OK
}
task<void> h()
{
co_await g();
// OK, implicit co_return;
}
Если выполнение корутины завершается необработанным исключением, она выполняет следующие действия:
- перехватывает исключение и вызывает
promise.unhandled_exception()из блока catch; - вызывает
promise.final_suspend()и применяет co_await к результату (например, чтобы возобновить продолжение или опубликовать результат). Возобновление корутины на этом этапе приводит к неопределённому поведению.
Когда состояние корутины уничтожается — из-за завершения через 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
{
/* ... */
// ensure the use of non-throwing operator-new
static Coroutine get_return_object_on_allocation_failure()
{
std::cerr << __func__ << '\n';
throw std::bad_alloc(); // or, return Coroutine(nullptr);
}
// custom non-throwing overload of new
void* operator new(std::size_t n) noexcept
{
if (void* mem = std::malloc(n))
return mem;
return nullptr; // allocation failure
}
};
Обещание
Тип Promise определяется компилятором на основе типа возвращаемого значения корутины с помощью std::coroutine_traits.
Формально, пусть
RиArgs...обозначают соответственно тип возвращаемого значения корутины и список типов её параметров;ClassTобозначает тип класса, которому принадлежит корутина, если она определена как нестатическая функция-член;- cv обозначает cv-квалификацию, указанную в объявлении функции, если она определена как нестатическая функция-член,
тогда тип Promise определяется следующим образом:
- std::coroutine_traits<R,Args...>::promise_type, если корутина не определена как функция-член с неявным объектом;
- std::coroutine_traits<R,cvClassT&,Args...>::promise_type, если корутина определена как функция-член с неявным объектом без квалификатора rvalue-ссылки;
- std::coroutine_traits<R,cvClassT&&,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>,constBar&,int>::promise_type |
task<void> Bar::foo(int x) &&; | std::coroutine_traits<task<void>,Bar&&,int>::promise_type |
co_await
Унарный оператор co_await приостанавливает корутину и возвращает управление вызывающей функции.
co_await expr |
Выражение co_await может встречаться только в потенциально вычисляемом выражении внутри обычного тела функции (включая тело функции лямбда-выражения) и не может встречаться
- в обработчике;
- в операторе объявления, если только оно не находится в инициализаторе этого оператора объявления;
- в простом объявлении init-statement (см.
if,switch,forи [[../range-for|range-for]]), если только оно не находится в инициализаторе этого init-statement ; - в аргументе по умолчанию или
- в инициализаторе переменной с областью видимости блока и статическим или потоковым временем хранения.
| Выражение co_await не может быть потенциально вычисляемым подвыражением предиката контрактного утверждения. | (начиная с C++26) |
Сначала expr преобразуется в объект, допускающий ожидание, следующим образом:
- если expr получено из начальной точки приостановки, конечной точки приостановки или выражения yield, объектом, допускающим ожидание, является само expr без изменений;
- в противном случае, если тип
Promiseтекущей корутины содержит функцию-членawait_transform, объектом, допускающим ожидание, являетсяpromise.await_transform(expr); - в противном случае объектом, допускающим ожидание, является само expr без изменений.
Затем следующим образом получается объект-ожидатель:
- если разрешение перегрузки для operatorco_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 — дескриптор корутины, представляющий текущую корутину. Внутри этой функции приостановленное состояние корутины доступно через этот дескриптор, а сама функция должна запланировать её возобновление в некотором исполнителе или уничтожение (возврат false считается планированием)- если
await_suspendвозвращает void, управление немедленно возвращается вызывающей функции или функции, возобновившей текущую корутину (эта корутина остаётся приостановленной); в противном случае - если
await_suspendвозвращает bool, - значение
trueвозвращает управление вызывающей функции или функции, возобновившей текущую корутину; - значение
falseвозобновляет текущую корутину. - если
await_suspendвозвращает дескриптор другой корутины, эта корутина возобновляется (вызовомhandle.resume()) (обратите внимание: это может запустить цепочку вызовов, которая в итоге приведёт к возобновлению текущей корутины); - если
await_suspendвыбрасывает исключение, оно перехватывается, корутина возобновляется, и исключение немедленно выбрасывается повторно.
- если
Наконец, вызывается awaiter.await_resume() (независимо от того, была ли корутина приостановлена), и его результат становится результатом всего выражения co_await expr.
Если корутина была приостановлена в выражении co_await и позднее возобновлена, выполнение продолжается непосредственно перед вызовом awaiter.await_resume().
Обратите внимание, что корутина полностью приостанавливается до входа в awaiter.await_suspend(). Её дескриптор можно передать другому потоку и возобновить до того, как функция await_suspend() вернёт управление. (Обратите внимание, что стандартные правила безопасности памяти по-прежнему применяются: если дескриптор корутины передаётся между потоками без блокировки, ожидатель должен использовать как минимум семантику release, а возобновляющий поток — как минимум семантику acquire.) Например, дескриптор корутины можно поместить в обратный вызов, запланированный для выполнения в пуле потоков после завершения асинхронной операции ввода-вывода. В этом случае, поскольку текущая корутина могла быть возобновлена и, следовательно, выполнить деструктор объекта-ожидателя, пока 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() { return false; }
void await_suspend(std::coroutine_handle<> h)
{
std::jthread& out = *p_out;
if (out.joinable())
throw std::runtime_error("Output jthread parameter not empty");
out = std::jthread([h] { h.resume(); });
// Potential undefined behavior: accessing potentially destroyed *this
// std::cout << "New thread ID: " << p_out->get_id() << '\n';
std::cout << "New thread ID: " << out.get_id() << '\n'; // this is 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 << "Coroutine started on thread: " << std::this_thread::get_id() << '\n';
co_await switch_to_new_thread(out);
// awaiter destroyed here
std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
}
int main()
{
std::jthread out;
resuming_on_new_thread(out);
}
Возможный вывод:
Coroutine started on thread: 139972277602112 New thread ID: 139972267284224 Coroutine resumed on thread: 139972267284224
Примечание: объект-ожидатель является частью состояния корутины (как временный объект, время жизни которого охватывает точку приостановки) и уничтожается до завершения выражения co_await. Его можно использовать для хранения состояния отдельной операции, необходимого некоторым API асинхронного ввода-вывода, без дополнительных динамических выделений памяти.
Стандартная библиотека определяет два тривиальных объекта, допускающих ожидание: std::suspend_always и std::suspend_never.
| Пример использования promise_type::await_transform и ожидателя, предоставленного программой |
|---|
Пример#include <cassert>
#include <coroutine>
#include <iostream>
struct tunable_coro
{
// An awaiter whose "readiness" is determined via constructor's parameter.
class tunable_awaiter
{
bool ready_;
public:
explicit(false) tunable_awaiter(bool ready) : ready_{ready} {}
// Three standard awaiter interface functions:
bool await_ready() const noexcept { return ready_; }
static void await_suspend(std::coroutine_handle<>) noexcept {}
static void await_resume() noexcept {}
};
struct promise_type
{
using coro_handle = std::coroutine_handle<promise_type>;
auto get_return_object() { return coro_handle::from_promise(*this); }
static auto initial_suspend() { return std::suspend_always(); }
static auto final_suspend() noexcept { return std::suspend_always(); }
static void return_void() {}
static void unhandled_exception() { std::terminate(); }
// A user provided transforming function which returns the custom awaiter:
auto await_transform(std::suspend_always) { return tunable_awaiter(!ready_); }
void disable_suspension() { ready_ = false; }
private:
bool ready_{true};
};
tunable_coro(promise_type::coro_handle h) : handle_(h) { assert(h); }
// For simplicity, declare these 4 special functions as deleted:
tunable_coro(tunable_coro const&) = delete;
tunable_coro(tunable_coro&&) = delete;
tunable_coro& operator=(tunable_coro const&) = delete;
tunable_coro& operator=(tunable_coro&&) = delete;
~tunable_coro()
{
if (handle_)
handle_.destroy();
}
void disable_suspension() const
{
if (handle_.done())
return;
handle_.promise().disable_suspension();
handle_();
}
bool operator()()
{
if (!handle_.done())
handle_();
return !handle_.done();
}
private:
promise_type::coro_handle handle_;
};
tunable_coro generate(int n)
{
for (int i{}; i != n; ++i)
{
std::cout << i << ' ';
// The awaiter passed to co_await goes to promise_type::await_transform which
// issues tunable_awaiter that initially causes suspension (returning back to
// main at each iteration), but after a call to disable_suspension no suspension
// happens and the loop runs to its end without returning to main().
co_await std::suspend_always{};
}
}
int main()
{
auto coro = generate(8);
coro(); // emits only one first element == 0
for (int k{}; k < 4; ++k)
{
coro(); // emits 1 2 3 4, one per each iteration
std::cout << ": ";
}
coro.disable_suspension();
coro(); // emits the tail numbers 5 6 7 all at ones
}
Вывод: 0 1 : 2 : 3 : 4 : 5 6 7 |
co_yield
Выражение co_yield возвращает значение вызывающей функции и приостанавливает текущую корутину: это основной строительный блок возобновляемых функций-генераторов.
co_yield expr | ||
co_yield braced-init-list |
Это эквивалентно
co_await promise.yield_value(expr)
Типичный yield_value генератора сохраняет (копирует, перемещает или просто сохраняет адрес, поскольку время жизни аргумента охватывает точку приостановки внутри co_await) свой аргумент в объекте генератора и возвращает std::suspend_always, передавая управление вызывающей функции или функции, возобновившей выполнение.
#include <coroutine>
#include <cstdint>
#include <exception>
#include <iostream>
template<typename T>
struct Generator
{
// The class name 'Generator' is our choice and it is not required for coroutine
// magic. Compiler recognizes coroutine by the presence of 'co_yield' keyword.
// You can use name 'MyGenerator' (or any other name) instead as long as you include
// nested struct promise_type with 'MyGenerator get_return_object()' method.
// (Note: It is necessary to adjust the declarations of constructors and destructors
// when renaming.)
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
struct promise_type // required
{
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(); } // saving
// exception
template<std::convertible_to<T> From> // C++20 concept
std::suspend_always yield_value(From&& from)
{
value_ = std::forward<From>(from); // caching the result in promise
return {};
}
void return_void() {}
};
handle_type h_;
Generator(handle_type h) : h_(h) {}
~Generator() { h_.destroy(); }
explicit operator bool()
{
fill(); // The only way to reliably find out whether or not we finished coroutine,
// whether or not there is going to be a next value generated (co_yield)
// in coroutine via C++ getter (operator () below) is to execute/resume
// coroutine until the next co_yield point (or let it fall off end).
// Then we store/cache result in promise to allow getter (operator() below
// to grab it without executing coroutine).
return !h_.done();
}
T operator()()
{
fill();
full_ = false; // we are going to move out previously cached
// result to make promise empty again
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_);
// propagate coroutine exception in called context
full_ = true;
}
}
};
Generator<std::uint64_t>
fibonacci_sequence(unsigned n)
{
if (n == 0)
co_return;
if (n > 94)
throw std::runtime_error("Too big Fibonacci sequence. Elements would overflow.");
co_yield 0;
if (n == 1)
co_return;
co_yield 1;
if (n == 2)
co_return;
std::uint64_t a = 0;
std::uint64_t b = 1;
for (unsigned i = 2; i < n; ++i)
{
std::uint64_t s = a + b;
co_yield s;
a = b;
b = s;
}
}
int main()
{
try
{
auto gen = fibonacci_sequence(10); // max 94 before uint64_t overflows
for (int j = 0; gen; ++j)
std::cout << "fib(" << j << ")=" << gen() << '\n';
}
catch (const std::exception& ex)
{
std::cerr << "Exception: " << ex.what() << '\n';
}
catch (...)
{
std::cerr << "Unknown exception.\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++.
| DR | Применяется к | Поведение согласно опубликованной версии | Корректное поведение |
|---|---|---|---|
| CWG 2556 | C++20 | некорректный return_void приводил к неопределённому поведению привыходе за конец корутины | в этом случае программа некорректна |
| CWG 2668 | C++20 | co_await нельзя было использовать в лямбда-выражениях | разрешено |
| CWG 2754 | C++23 |
*this использовался при создании объекта обещаниядля функций-членов с явным объектом |
*this в этом случаене используется |
См. также
|
(C++26) | представляет отправителя, который можно использовать как тип возвращаемого значения функций-корутин (шаблон класса) |
|
(C++23) | view, представляющее генератор синхронных корутин (шаблон класса) |
|
(C++20) | тип-признак для определения типов обещаний корутин (шаблон класса) |
Внешние ссылки
| 1. | Lewis Baker, 2017–2022 — Асимметричная передача управления. |
| 2. | David Mazières, 2021 — Руководство по корутинам C++20. |
| 3. | Chuanqi Xu & Yu Qi & Yao Han, 2021 — Принципы и применение корутин в C++20. (на китайском) |
| 4. | Simon Tatham, 2023 — Создание пользовательских систем корутин C++20. |
© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/cpp/language/coroutines