协程(coroutine)是一种可以暂停执行以便稍后恢复的函数。协程是无堆栈的(stackless),除非编译器对其进行了优化,否则它们的状态将分配在堆上:它们通过返回给调用者来暂停执行,并且恢复执行所需的数据与堆栈分开存储。这允许异步执行顺序代码(sequential code)(例如,无需显式回调即可处理非阻塞I/O),并且还支持惰性计算无限序列(lazy-computed infinite sequences)上的算法和其他用途。
如果函数定义包含以下任何一项,则该函数为协程:即要定义协程,函数主体中必须存在co_await、co_yield或co_return关键字。
(1).co_await表达式:暂停执行直到恢复。一元运算符co_await暂停协程并将控制权返回给调用者。其操作数是一个表达式,该表达式要么属于定义成员运算符co_await的类类型,要么可以传递给非成员运算符co_await,或者可以通过当前协程的Promise::await_transform转换为这样的类类型。
(2).co_yield表达式:暂停执行并返回一个值。类似于Python中的yield。co_yield表达式向调用者返回一个值并暂停当前协程:它是可恢复生成器函数(resumable generator functions)的通用构建块。
(3).co_return语句:完成执行并返回一个值。
每个协程都必须具有满足如下所述的一些要求的返回类型:
(1).限制(Restrictions):协程不能使用可变参数、普通返回语句或占位符返回类型(auto或concept)。consteval函数、constexpr函数、构造函数、析够函数和main函数不能是协程。
(2).执行(Execution):
每个协程都与以下对象相关联:
promise对象:从协程内部进行操作。协程通过此对象提交其结果或异常。promise对象与std::promise没有任何关系。
协程句柄:从协程外部进行操作。这是一个非拥有句柄,用于恢复协程的执行或销毁协程框架。
协程状态:即内部的、动态分配的存储(除非优化了分配),包含的对象:promise对象;参数(全部按值拷贝);当前暂停点的一些表示,以便恢复知道从哪里继续,销毁知道范围内有哪些局部变量;局部变量和临时变量,其生存期跨越当前暂停点。
当协程开始执行时,它会执行以下操作:
使用operator new分配协程状态对象。
将所有函数参数复制到协程状态:值参数被移动或复制,引用参数保持引用(by-value parameters are moved or copied, by-reference parameters remain references)。
调用promise对象的构造函数。如果promise类型有一个接受所有协程参数的构造函数,则使用复制后(post-copy)的协程参数调用该构造函数。否则,将调用默认构造函数。
调用promise.get_return_object()并将结果保存在局部变量中。当协程首次暂停时,该调用的结果将返回给调用者。在此步骤之前(包括此步骤)抛出的任何异常都会传播回调用者,而不是放置在promise中。
调用promise.initial_suspend()并co_awaits其结果。典型的promise类型要么返回std::suspend_always(用于延迟启动的协程),要么返回 std::suspend_never(用于急切启动的协程)。
当co_await promise.initial_suspend()恢复时,开始执行协程的主体。
当协程到达暂停点时:
如果有必要,将先前获得的返回对象隐式转换为协程的返回类型后,返回给调用者/恢复者(caller/resumer)。
当协程到达co_return语句时,它会执行以下操作:
调用promise.return_void()来获取co_return; co_return expr; 其中expr的类型为void
或对co_return expr; 调用promise.return_value(expr);其中expr具有非void类型
按照创建时的相反顺序销毁所有具有自动存储持续时间(automatic storage duration)的变量。
调用promise.final_suspend()并co_awaits结果。
如果协程以未捕获的异常结束,它将执行以下操作:
捕获异常并从catch块中调用promise.unhandled_exception()
调用promise.final_suspend()并co_awaits结果。从此时恢复协程是未定义的行为。
当协程状态由于通过co_return或未捕获的异常终止或者由于通过其句柄被销毁而被销毁时,它会执行以下操作:
调用promise对象的析构函数。
调用函数参数副本(function parameter copies)的析构函数。
调用operator delete释放协程状态使用的内存。
将执行权转回给调用者/恢复者。
(3).动态分配(Dynamic allocation):
协程状态通过non-array operator new动态分配。
如果Promise类型定义了类级别的替换(class-level replacement),则会使用它,否则将使用全局operator new。
如果Promise类型定义了一个采用附加参数的operator new的放置形式(placement form),并且它们与一个参数列表匹配,其中第一个参数是请求的大小,其余的是协程函数参数,那么这些参数将被传递给operator new。
如果分配失败,协程将抛出std::bad_alloc,除非Promise类型定义成员函数Promise::get_return_object_on_allocation_failure()。如果定义了该成员函数,则分配使用operator new的nothrow形式,并且在分配失败时,协程会立即将从Promise::get_return_object_on_allocation_failure()获得的对象返回给调用者。
Promise:Promise类型由编译器使用std::coroutine_traits根据协程的返回类型来确定。
在C++23中支持了std::generator后,再使用协程会方便很多。
以下为测试代码:主要来自于cppreference
namespace {
// reference: https://en.cppreference.com/w/cpp/language/coroutines
template<typename T>
struct Generator { // In C++23, you can use #include <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.
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<int> range(int start, int end)
{
while (start < end) {
co_yield start;
start++;
}
// Implicit co_return at the end of this function:
// co_return;
}
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;
}
}
} // namespace
int test_coroutine()
{
auto gen = range(0, 10);
for (int j = 0; gen; ++j)
std::cout << "value: " << gen() << '\n';
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";
}
return 0;
}
执行结果如下图所示: