协程是能暂停执行以再之后恢复的函数,C++协程是无栈的:它们通过返回到调用方暂停执行,并且恢复执行所需的数据与栈分离存储,这样就可以编写异步执行的顺序代码【1】;
但使用起来还是需要一些学习成本,本文主要对C++协程的使用进行总结。
C++20中协程
C++20中提供了协程的支持,一个函数中包含co_await、co_yield、co_return关键字则都是协程,从一个简单的协程例子【2】入手分析协程的工作流程:
struct ReturnObject {
struct promise_type {
ReturnObject get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void unhandled_exception() {}
};
};
struct Awaiter {
std::coroutine_handle<> *hp_;
constexpr bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) { *hp_ = h; }
constexpr void await_resume() const noexcept {}
};
ReturnObject counter(std::coroutine_handle<> *continuation_out)
{
Awaiter a{continuation_out};
for (int32_t i = 0;; ++i) {
co_await a;
std::cout << "counter: " << i << std::endl;
}
}
int main()
{
std::coroutine_handle<> h;
counter(&h);
for (int i = 0; i < 3; ++i) {
std::cout << "In main function\n";
h();
}
h.destroy();
return 0;
}
In main function
counter: 0
In main function
counter: 1
In main function
counter: 2
从输出上看counter函数通过协程挂起与恢复了三次;
代码中使用co_await关键字,同时又看到了很多额外的类与定义,包括ReturnObject、promise_type、Awaiter等,但这些之间是什么关系?
std::coroutine_handle
协程句柄封装了协程帧指针,提供了协程恢复、销毁以及与获取协程的promise_type类型对象的接口;
std::coroutine_handle 是一个模板类,查看其泛化版本提供的接口
_EXPORT_STD template <class _Promise>
struct coroutine_handle {
constexpr coroutine_handle() noexcept = default;
constexpr coroutine_handle(nullptr_t) noexcept {}
// 通过promise对象构造一个协程句柄
_NODISCARD static coroutine_handle from_promise(_Promise& _Prom) noexcept;
coroutine_handle& operator=(nullptr_t) noexcept;
_NODISCARD constexpr void* address() const noexcept;
_NODISCARD static constexpr coroutine_handle from_address(void* const _Addr) noexcept;
// 获取类型擦除句柄,即用户不关心promise对象
constexpr operator coroutine_handle<>();
// 判断协程是否结束
constexpr explicit operator bool();
// 判断协程是否结束
_NODISCARD bool done() const noexcept;
// 恢复协程
void operator()() const;
// 恢复协程
void resume() const;
// 销毁协程
void destroy() const noexcept;
// 获取promise对象的引用
_NODISCARD _Promise& promise() const noexcept;
};
coroutine_handle和promise对象可以相互转换,from_promise和promise接口;
std::coroutine_handle<promise_type> handle;
handle_.promise() // 通过handle访问promise,promise对象的引用
std::coroutine_handle<promise_type>::from_promise(*this) // 通过promise获取handle
例如修改上述例子,通过promise得到handle存在ReturnObject中:
struct ReturnObject {
struct promise_type {
ReturnObject get_return_object() {
return {
// Uses C++20 designated initializer syntax
.h_ = std::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void unhandled_exception() {}
};
std::coroutine_handle<promise_type> h_;
operator std::coroutine_handle<promise_type>() const { return h_; }
// A coroutine_handle<promise_type> converts to coroutine_handle<>
operator std::coroutine_handle<>() const { return h_; }
};
promise_type
C++异步编程中介绍了future和promise,通过promise可以获取到一个future,promise存储异步计算的值,通过future可以获取到;
在上述例子中,编译期会检查协程用户自定义返回类型ReturnObject中是否定了promise_type
template <typename _Result, typename = void>
struct __coroutine_traits_impl {};
template <typename _Result>
struct __coroutine__impl<_Result,
__void_t<typename _Result::promise_type>>
{
using promise_type = typename _Result::promise_type;
};
template <typename _Result, typename...>
struct coroutine_traits : __coroutine_traits_impl<_Result> {};
同时编译期会检查是否存在get_return_object返回对应ReturnObject,ReturnObject即为future/promise中的future,promise即为上述例子中的promise_type;
在运行期通过构造promise_type对象,通过get_return_object创建ReturnObject对象;
编译期代码变换
在编译期编译器会对协程进行代码变换,上述例子中代码转换后为:
ReturnObject counter(std::coroutine_handle<> *continuation_out)
{
... // 构造协程栈帧,申请内存,实参拷贝,promise_type、ReturnObject构造
// ReturnType::promise_type构造promise_type
// 构造ReturnObject,promise.get_return_object();
co_await promise.initial_suspend();
try {
Awaiter a{continuation_out};
for (int32_t i = 0;; ++i) {
co_await a; // 进一步转换
std::cout << "counter: " << i << std::endl;
}
} catch (...) {
promise.unhandled_exception();
}
...// 释放协程局部变量
// 若没有co_return,则会协程体结束隐式插入co_return
final-suspend:
co_await promise.final-suspend();
}
代码中的co_await a => co_await <expr> 变换为:
{
auto&& value = <expr>; // expr 执行返回awaitable 对象
auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));
if (!awaiter.await_ready())
{
using handle_t = std::experimental::coroutine_handle<P>;
using await_suspend_result_t =
decltype(awaiter.await_suspend(handle_t::from_promise(p)));
<suspend-coroutine> // 在await_suspend之前就将协程的状态标记为挂起
if constexpr (std::is_void_v<await_suspend_result_t>)
{
awaiter.await_suspend(handle_t::from_promise(p)); // 将promise对象返回给
<return-to-caller-or-resumer>
}
else
{
static_assert(
std::is_same_v<await_suspend_result_t, bool>,
"await_suspend() must return 'void' or 'bool'.");
if (awaiter.await_suspend(handle_t::from_promise(p))) // 返回为false,则恢复当前协程
{
<return-to-caller-or-resumer>
}
}
<resume-point> // 协程在 co_await 表达式中暂停而在后来恢复,那么恢复点处于紧接对 awaiter.await_resume() 的调用之前
}
return awaiter.await_resume();
}
其中get_awaitable和get_awaiter为:
template<typename P, typename T>
decltype(auto) get_awaitable(P& promise, T&& expr)
{
if constexpr (has_any_await_transform_member_v<P>)
return promise.await_transform(static_cast<T&&>(expr));
else
return static_cast<T&&>(expr);
}
template<typename Awaitable>
decltype(auto) get_awaiter(Awaitable&& awaitable)
{
if constexpr (has_member_operator_co_await_v<Awaitable>)
return static_cast<Awaitable&&>(awaitable).operator co_await();
else if constexpr (has_non_member_operator_co_await_v<Awaitable&&>)
return operator co_await(static_cast<Awaitable&&>(awaitable));
else
return static_cast<Awaitable&&>(awaitable);
}
在变换后的代码中可以看到,promise对象构造之后通过get_return_object获取ReturnObject,也说明了为什么promise_type中需要initial_suspend、final_suspend、unhandled_exception等函数。
对于协程体中的实参要注意:在协程帧分配内存后,将调用者传递给协程的实参拷贝到协程帧中,以便后续协程挂起后仍能访问这些实参,如果是引用传递则无法保证调用者的临时变量的生命周期,导致协程中访问引用出现非法内存访问;【3】
co_await
co_await <exp> 表达式需要返回一个awaiter对象,在转换后的代码中,表达式先进行执行,返回一个awaitable对象,首先检查promise对象中是否存在await_transform方法,获取awaitable对象;再检查awaitable对象是否重载了co_await操作符并返回最终的awaiter对象【3】,上述例子中自定义了Awaiter类型。
在执行协程代码之前有:
co_await promise.initial_suspend();
initial_suspend接口允许在执行协程的代码之前判断是否需要挂起,返回的awaitable对象标准库中提供了两种默认实现:
struct suspend_always
{
constexpr bool await_ready() const noexcept { return false; } // 挂起
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
struct suspend_never
{
constexpr bool await_ready() const noexcept { return true; } // 不挂起
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
而co_await <exp>整个表达式的返回值最终为awaiter.await_resume();
在co_await转换后的代码中,调用 awaiter.await_ready()返回 false,那么暂停协程;
调用 awaiter.await_suspend(handle),其中handle是表示当前协程的协程句柄,可以通过这个句柄观察暂停的协程,而且此函数负责调度它以在某个执行器上恢复,或将其销毁【1】; 如果 await_suspend 返回 void,那么立即将控制返回给当前协程的调用方/恢复方(此协程保持暂停),否则 如果 await_suspend 返回 bool,那么: 值为 true 时将控制返回给当前协程的调用方/恢复方 值为 false 时恢复当前协程。 如果 await_suspend 返回某个其他协程的协程句柄,那么(通过调用 handle.resume())恢复该句柄;
在【4】中介绍了await_suspend 其他协程句柄的设计避免了栈溢出问题
结合编译期转换后的代码,总结协程流程如下:
分析第一节中的示例代码:
step1: 调用counter,构造promise对象与协程相关状态参数,注意promise_type要在Task中定义
step2: 由于init_suspend返回std::suspend_never,因此不会挂起
step3: 运行到co_await a
step4: Awaiter 中await_ready返回false,因此挂起协程,将ReturnObject返回给调用者
step5: 进行main函数中的循环,调用handle的resume,恢复协程
step6: 恢复协程状态,调用对应Awaiter对象的await_resume方法,重新调用到step2
由于协程体for循环没有结束条件,因此不会执行到promise的final_suspend方法,直到main函数中执行handle的destroy,promise对象生命周期结束,销毁协程相关参数
co_yield
co_yield <expr> 等价与 co_await promise.yield_value(expr);
在理解了第一节中的代码执行流程后,后面就比较好理解了,promise_type中需要增加yield_value方法,入参类型需要和<expr>返回的类型一致,这次重新修改一个代码例子进行分析:
struct ReturnObject {
struct promise_type {
struct FinalAwaiter {
bool await_ready() const noexcept { return true; }
void await_suspend(std::coroutine_handle<promise_type> h) noexcept { }
void await_resume() noexcept {
DBG_LOG();
}
};
struct Awaiter {
~Awaiter()
{
DBG_LOG("~Awaiter");
}
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<promise_type> h) noexcept {
DBG_LOG();
}
void await_resume() const noexcept {}
};
promise_type()
{
DBG_LOG("%p", this);
}
~promise_type()
{
DBG_LOG("%p", this);
}
ReturnObject get_return_object() {
return {.h_ = std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend()
{
DBG_LOG();
return {};
}
FinalAwaiter final_suspend() noexcept
{
DBG_LOG();
return {};
}
Awaiter yield_value(unsigned value) {
DBG_LOG();
value_ = value;
return {};
}
void return_void()
{
DBG_LOG();
}
void unhandled_exception() {}
uint32_t value_;
};
std::coroutine_handle<promise_type> h_;
};
ReturnObject counter()
{
co_yield 10;
}
int main()
{
auto r = counter();
r.h_();
return 0;
}
step1: 调用counter,构造promise对象与协程相关状态参数
step2: 由于init_suspend返回std::suspend_never,因此不会挂起
step3: 运行到co_yield 10,即调用co_await promise.yield_value(10)
step4: 调用promise_type::yield_value,返回Awaiter
step5: Awaiter 中await_ready返回false,因此挂起协程,将ReturnObject返回给调用者
step6: 回到main函数,拿到ReturnObject,resume协程
step7: 恢复协程状态,调用对应Awaiter对象的await_resume方法;
Awaiter对象开始析构
step8: 协程状态转换为done,析构协程函数中的局部变量;隐式插入co_return,调用promise.return_void()
step9: 调用到co_await promise.final_suspend(),在此之前协程状态转换为done
step10: 由于FinalAwaiter::await_ready返回true,则协程不再挂起,运行到FinalAwaiter::await_resume
step11: promise_type析构,销毁协程实参,协程自动销毁,后续不能再通过handle进行destroy,否则会出现双重释放
这里修改FinalAwaiter::await_ready返回false,则不会再调用到FinalAwaiter::await_resume,协程挂起;
而此时step9的时候协程状态已经为done,不能再调用handle resume,这个时候协程交由调用者进行手动销毁,通常将ReturnObject修改为RAII类,在其析构函数中自动调用handle destroy;
~ReturnObject() {
DBG_LOG();
h_.destroy();
}
对于协程实参拷贝,可以看测试代码:
如果counter中送入是Test引用,协程将传递引用给协程,在前文“编译期代码变换”中已有说明会可能导致非法内存访问;
co_return
co_return 相当于调用了 promise.return_value() 或者 promise.return_void() 然后跳到 final-suspend 标签那里, 也就是说这个这个协程结束了, 再也无法被恢复了。在进入final_suspend之前,会逆序析构协程体内的局部变量,但这时候不会析构协程的实参【3】;
流程与上面分析类似,在此不再赘述;
限制
协程不能使用变长实参,普通的 return 语句,或占位符返回类型(auto 或 Concept)。
constexpr 函数、构造函数、析构函数及 main 函数 不能是协程【1】。
参考资料
【1】https://en.cppreference.com/w/cpp/language/coroutines
【2】 My tutorial and take on C++20 coroutines (stanford.edu)
【3】C++20高级编程
【4】https://lewissbaker.github.io/2020/05/11/understanding_symmetric_transfer
标签:suspend,handle,await,介绍,return,promise,协程 From: https://blog.51cto.com/u_13137973/7974396