前言

之前写过一篇 C++20 协程入门的文章:《使用 C++ 20 协程降低异步网络编程复杂度》,谈到了协程在消除异步编程 callback hell 方面的重要贡献——使代码更清晰,易于维护;以及 C++20 协程具有无栈、非对称等特性。无栈协程具有不受预分配栈空间约束、切换类似函数开销更小的优点,符合 C++ 语言设计原则中的 no payload 理念 (不因新增加的语言特性而增加额外性能负担);非对称表示协程控制权的转移是单向的,即通过 co_await/co_yield 挂起时,必需返回到调用者最初的上下文,而不能随意切换到其它协程,这样做逻辑清晰,便于调试。

C++20 协程相对的缺点就是概念繁多、过于灵活,特别是编译器在底层默默的做了很多工作,使得调用链经常断掉不好理解,之前的文章讲到原理就草草贴了几张流程图了事,今天要把这个原理掰开了好好说道一番。

讲 C++20 协程,除了协程本身的复杂性,还有新标准带来的新特性,每次新的标准面世,就像是换了个语言,各种语法糖能大大提升开发效率,但也提升了理解成本。以插入 map 元素这个小功能为例,看看各个标准是如何演化的。

我们知道,std::map 在 insert 时如果元素已经存在是不会替换元素的,而是返回一个指示元素所在位置的 iterator 和是否插入成功的标志:

#include <iostream>
#include <map> int main() {
std::map<int, int> mp;
// mp.insert(std::make_pair(1, 2));
std::pair<std::map<int, int>::iterator, bool> result = mp.insert(std::make_pair(1, 1));
if (result.second)
std::cout << "inserted" << std::endl; for (std::map<int, int>::iterator itr = mp.begin(); itr != mp.end(); ++itr) {
std::cout << "{" << itr->first << ", " << itr->second << "}" << std::endl;
} return 0;
}

输出:

inserted
{1, 1}

这是 C++98 标准就支持的语法,map::insert 返回值为 std::pair,其 first 为容器 iterator 用于标识插入或已有元素位置,其 second 为 bool 表示是否插入成功。下面看下 C++11 的改进:

#include <iostream>
#include <map>
#include <tuple> int main() {
std::map<int, int> mp; // = { {1,3} };
bool inserted;
std::tie(std::ignore, inserted) = mp.insert({1, 1});
if (inserted)
std::cout << "inserted" << std::endl; // for (auto itr = mp.begin(); itr != mp.end(); ++itr) {
for(auto itr : mp) {
std::cout << "{" << itr.first << ", " << itr.second << "}" << std::endl;
}
return 0;
}

输出一致。主要改进在于通过 tie 将 inserted 变量绑定到返回的 tuple 结构中 (pair 也是 tuple 的一种),之后直接引用 inserted 变量,而不是不明就里的 first & second,代码可读性更强了并且没有额外的对象拷贝。这个 demo 还展示了 C++11 引入的其它特性,如:

* 聚合初始化 :std::map<int, int> mp; // = { {1,1} };  & mp.insert({1, 1});

* 类型自动推导:// for (auto itr = mp.begin(); itr != mp.end(); ++itr)

* 范围 for 循环:for(auto itr : mp) 

等。下面看下 C++17 的改进:

#include <iostream>
#include <map> int main()
{
std::map<int, int> map; // = { {1,4} };
auto&& [itr, inserted] = map.insert({ 1, 1 });
if (inserted)
std::cout << "inserted" << std::endl; for (auto&& [k, v] : map)
std::cout << "{" << k << ", " << v << "}" << std::endl;
}

输出不变。相比 C++17,这里连 inserted 变量也不需要定义了,通过结构化绑定,直接原地定义返回的两个分量 (itr & inserted);另外在遍历 map 元素时,也通过结构化绑定直接获取 first & second (k & v),代码更简洁了。但对于一个不怎么关注新标准的老鸟,这是不是就有阅读障碍了?加之这种语言层面的变动多而细碎,如果打算先了解语法再深入协程,就很容易导致从入门到放弃的学习过程。

为了将这个先有鸡先有蛋的乱麻问题破解掉,本文遵循以下原则:

* 以协程为目标,涉及到的新语法会简单说明,不涉及的不旁征博引

* 若语法的原理非常简单,也会简单展开讲讲,有利于了解其本质

另外选取合适的 demo 也非常重要,太复杂的一下讲不清容易有挫折感,太简单的看了不知道有何用处也是一头雾水,本文选取的 demo 将在贴合实际的基础上尽量简化,以突出问题核心。

最后说说工具的问题,自己搭建环境费时费力,现成的则不一定有合适的编译器版本,这里推荐两个工具:

* Compile Explorer:在线编译 C++ 代码工具,查看汇编结果与运行结果,可切换编译器及版本、增加编译选项

* C++ Insights:也是编译工具,但不是生成汇编代码而是 C++ 表达的中间代码,可以用来查看 C++ 编译器底层做的一些工作,对于本文的主题 C++20 协程至关重要

其实好多语法糖丢这里可以一眼露馅,比如上面的结构化绑定,其实在底层用的还是 std::pair,只不过编译器帮你省略了繁锁的细节,这比看反汇编是直观多了。

协程本质

在进入 C++20 协程之前,有必要搞懂协程本身是什么,它能让出控制权、能继续执行、没有线程栈的切换,看起来似乎很神奇,一般函数可没有这个能力。

早年间 C++17 的协程就是通过 duff device (switch case) 实现的:

void fn(){
int a, b, c;
a = b + c;
yield();
b = c + a;
yield();
c = a + b;
}

其中 yield 就是协程让出控制权的点位,转换后变为这样:

Struct fn{
int a, b, c;
int __state = 0; void resume(){
switch(__state) {
case 0:
return fn1();
case 1:
return fn2();
case 2:
return fn3();
}
} void fn1(){
a = b + c;
__state ++;
} void fn2(){
b = c + a;
__state ++;
} void fn3(){
c = a + b;
__state ++;
}
};

所以 yield 其实就是函数 return,而协程本质就是函数+状态机,这个之前文章里都已经说过了,那 C++20 协程有本质不同吗?答案是没区别。下面来看一个典型的 C++20 协程例子,并根据编译器中间结果来印证上面的结论。

#include <coroutine>
#include <iostream> struct Generator {
struct promise_type {
int current_value;
auto get_return_object() { return Generator{this}; }
auto initial_suspend() { return std::suspend_always{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
void unhandled_exception() {}
auto yield_value(int value) {
current_value = value;
return std::suspend_always{};
}
}; std::coroutine_handle<promise_type> handle;
Generator(promise_type* p) : handle(std::coroutine_handle<promise_type>::from_promise(*p)) {}
~Generator() { if (handle) handle.destroy(); }
bool next() { return !handle.done() && (handle.resume(), !handle.done()); }
int value() { return handle.promise().current_value; }
}; Generator range(int from, int to) {
for (int i = from; i <= to; ++i) {
co_yield i;
}
} int main() {
auto gen = range(1, 5);
while (gen.next()) {
std::cout << gen.value() << std::endl;
}
}

这个例子演示了一个数列生成器,运行有如下输出:

1
2
3
4
5

其中协程体 range 十分短小精悍:

Generator range(int from, int to) {
for (int i = from; i <= to; ++i) {
co_yield i;
}
}

通过 co_yeild 不停的返回数列值。协程的返回类型Generator 是关键,称作返回对象,它要实现一系列接口,可以看做是 C++20 协程与用户的一个约定,这点就如同任意一个 C++ 类,实现了 operator() 接口就能被当作函数对象一样。凡是写 C++20 协程,必离不开返回对象,它内部又有两个约定:

* struct promise_type承诺对象。定义于返回对象内部的 traits 类型,用于定制协程行为,由用户实现,会被协程体访问

* std::coroutine_handle<promise_type> handle协程句柄。用于控制协程体的运行,由编译器实现,用户访问

这里暂不展开解释 Generator 的各个成员功用,反正就把它当成一个模板,写协程抄上就完事儿。

先了解下 main 是如何运转起来的,主要关注Generator::next 方法:

int main() {
auto gen = range(1, 5);
while (gen.next()) {
std::cout << gen.value() << std::endl;
}
}

它通过协程句柄的resume&done来驱动协程运转:

    bool next() { return !handle.done() && (handle.resume(), !handle.done()); }

main 其实就是 next 的循环,直到协程彻底完结,因此 demo 实际上演示了协程的 5 次进入和 5 次离开。

demo 底层是如何实现的?循环变量是如何恢复的?带着这些疑问,有请 Insights C++ 上场,看看这个 demo 的原形 (注意开启 Show coroutine transformation 选项):

查看代码
 /*************************************************************************************
* NOTE: The coroutine transformation you've enabled is a hand coded transformation! *
* Most of it is _not_ present in the AST. What you see is an approximation. *
*************************************************************************************/
#include <coroutine>
#include <iostream> struct Generator
{
struct promise_type
{
int current_value;
inline Generator get_return_object()
{
return Generator{this};
} inline std::suspend_always initial_suspend()
{
return std::suspend_always{};
} inline std::suspend_always final_suspend() noexcept
{
return std::suspend_always{};
} inline void unhandled_exception()
{
} inline std::suspend_always yield_value(int value)
{
this->current_value = value;
return std::suspend_always{};
} // inline constexpr promise_type() noexcept = default;
}; std::coroutine_handle<promise_type> handle;
inline Generator(promise_type * p)
: handle{std::coroutine_handle<promise_type>::from_promise(*p)}
{
} inline ~Generator() noexcept
{
if(this->handle.operator bool()) {
this->handle.destroy();
} } inline bool next()
{
return !this->handle.done() && (this->handle.resume() , !this->handle.done());
} inline int value()
{
return this->handle.promise().current_value;
} }; struct __rangeFrame
{
void (*resume_fn)(__rangeFrame *);
void (*destroy_fn)(__rangeFrame *);
std::__coroutine_traits_impl<Generator>::promise_type __promise;
int __suspend_index;
bool __initial_await_suspend_called;
int from;
int to;
int i;
std::suspend_always __suspend_24_11;
std::suspend_always __suspend_26_9;
std::suspend_always __suspend_24_11_1;
}; Generator range(int from, int to)
{
/* Allocate the frame including the promise */
/* Note: The actual parameter new is __builtin_coro_size */
__rangeFrame * __f = reinterpret_cast<__rangeFrame *>(operator new(sizeof(__rangeFrame)));
__f->__suspend_index = 0;
__f->__initial_await_suspend_called = false;
__f->from = std::forward<int>(from);
__f->to = std::forward<int>(to); /* Construct the promise. */
new (&__f->__promise)std::__coroutine_traits_impl<Generator>::promise_type{}; /* Forward declare the resume and destroy function. */
void __rangeResume(__rangeFrame * __f);
void __rangeDestroy(__rangeFrame * __f); /* Assign the resume and destroy function pointers. */
__f->resume_fn = &__rangeResume;
__f->destroy_fn = &__rangeDestroy; /* Call the made up function with the coroutine body for initial suspend.
This function will be called subsequently by coroutine_handle<>::resume()
which calls __builtin_coro_resume(__handle_) */
__rangeResume(__f); return __f->__promise.get_return_object();
} /* This function invoked by coroutine_handle<>::resume() */
void __rangeResume(__rangeFrame * __f)
{
try
{
/* Create a switch to get to the correct resume point */
switch(__f->__suspend_index) {
case 0: break;
case 1: goto __resume_range_1;
case 2: goto __resume_range_2;
case 3: goto __resume_range_3;
} /* co_await insights.cpp:24 */
__f->__suspend_24_11 = __f->__promise.initial_suspend();
if(!__f->__suspend_24_11.await_ready()) {
__f->__suspend_24_11.await_suspend(std::coroutine_handle<Generator::promise_type>::from_address(static_cast<void *>(__f)).operator std::coroutine_handle<void>());
__f->__suspend_index = 1;
__f->__initial_await_suspend_called = true;
return;
} __resume_range_1:
__f->__suspend_24_11.await_resume();
for(__f->i = __f->from; __f->i <= __f->to; ++__f->i) { /* co_yield insights.cpp:26 */
__f->__suspend_26_9 = __f->__promise.yield_value(__f->i);
if(!__f->__suspend_26_9.await_ready()) {
__f->__suspend_26_9.await_suspend(std::coroutine_handle<Generator::promise_type>::from_address(static_cast<void *>(__f)).operator std::coroutine_handle<void>());
__f->__suspend_index = 2;
return;
} __resume_range_2:
__f->__suspend_26_9.await_resume();
} goto __final_suspend;
} catch(...) {
if(!__f->__initial_await_suspend_called) {
throw ;
} __f->__promise.unhandled_exception();
} __final_suspend: /* co_await insights.cpp:24 */
__f->__suspend_24_11_1 = __f->__promise.final_suspend();
if(!__f->__suspend_24_11_1.await_ready()) {
__f->__suspend_24_11_1.await_suspend(std::coroutine_handle<Generator::promise_type>::from_address(static_cast<void *>(__f)).operator std::coroutine_handle<void>());
__f->__suspend_index = 3;
return;
} __resume_range_3:
__f->destroy_fn(__f);
} /* This function invoked by coroutine_handle<>::destroy() */
void __rangeDestroy(__rangeFrame * __f)
{
/* destroy all variables with dtors */
__f->~__rangeFrame();
/* Deallocating the coroutine frame */
/* Note: The actual argument to delete is __builtin_coro_frame with the promise as parameter */
operator delete(static_cast<void *>(__f), sizeof(__rangeFrame));
} int main()
{
Generator gen = range(1, 5);
while(gen.next()) {
std::cout.operator<<(gen.value()).operator<<(std::endl);
} return 0;
}

内容比较长,从头到尾分块解析一下。

承诺对象部分,一对一,比较直观,不多做解释了。

返回对象部分,也是如此,其中包含了协程句柄和一些自定义的接口。

注意返回对象的构造函数,接收一个承诺对象指针,并将其设置到协程句柄,该参数将由编译器构造并传入。

struct __rangeFrame
{
void (*resume_fn)(__rangeFrame *);
void (*destroy_fn)(__rangeFrame *);
std::__coroutine_traits_impl<Generator>::promise_type __promise;
int __suspend_index;
bool __initial_await_suspend_called;
int from;
int to;
int i;
std::suspend_always __suspend_24_11;
std::suspend_always __suspend_26_9;
std::suspend_always __suspend_24_11_1;
};

__rangeFrame称为协程状态,是由编译器生成的,没有对应的用户代码,用于保存协程体相关的必要信息:

* resume_fn&destroy_fnresume&destroy 回调,用于继续执行或销毁协程体

* __promise:用户提供的承诺对象,用于定制协程行为

* __suspend_index:duff device 状态机的状态值

* __initial_await_suspend_called:不重要忽略

* from&to&i:协程体参数 & 栈变量

* __suspend_24_11&__suspend_26_9&__suspend_24_11_1:协程挂起点的等待对象

下面看看它是如何初始化的:

Generator range(int from, int to)
{
/* Allocate the frame including the promise */
/* Note: The actual parameter new is __builtin_coro_size */

构建协程状态

  __rangeFrame * __f = reinterpret_cast<__rangeFrame *>(operator new(sizeof(__rangeFrame)));

初始化状态机

  __f->__suspend_index = 0;
__f->__initial_await_suspend_called = false;

初始化协程参数

  __f->from = std::forward<int>(from);
__f->to = std::forward<int>(to);

原地构建承诺对象 (只调构建函数不分配内存)

  /* Construct the promise. */
new (&__f->__promise)std::__coroutine_traits_impl<Generator>::promise_type{};

初始化协程控制接口

  /* Forward declare the resume and destroy function. */
void __rangeResume(__rangeFrame * __f);
void __rangeDestroy(__rangeFrame * __f); /* Assign the resume and destroy function pointers. */
__f->resume_fn = &__rangeResume;
__f->destroy_fn = &__rangeDestroy;

协程的第一次进入,就交给 resume 吧

  /* Call the made up function with the coroutine body for initial suspend.
This function will be called subsequently by coroutine_handle<>::resume()
which calls __builtin_coro_resume(__handle_) */
__rangeResume(__f);

协程的第一次离开,需要 return 返回对象

  return __f->__promise.get_return_object();
}

注意原协程函数已经被掏空,实际上放置的是协程状态等各种对象的初始化代码,协程的第一次进入是通过直接调用协程体的 resume 完成的,因此重点其实转移到了 __rangeResume 这个编译器生成的方法中,这个稍后详述。接着往下就是离开协程的 return 语句,它会通过promise_type::get_return_object来返回返回对象:

        auto get_return_object() { return Generator{this}; }

这里用到了聚合初始化,将 this 代表的承诺对象传递给了返回对象:

    Generator(promise_type* p) : handle(std::coroutine_handle<promise_type>::from_promise(*p)) {}

返回对象会将承诺对象保存在协程句柄中以待后用,这个代码之前贴过就不赘述了。

上面这张图展示了协程体、协程状态、承诺对象、返回对象、协程句柄之间的关系,这里补充一下协程句柄coroutine_handle的代码:

template<> struct coroutine_handle<void> {
void* m_ptr;
bool done() { return __builtin_coro_done(m_ptr); }
void resume() { __builtin_coro_resume(m_ptr); }
void operator()() { resume(); }
void destroy() { __builtin_coro_destroy(m_ptr); }
operator bool() { return bool(m_ptr); }
void* address() { return m_ptr; }
static coroutine_handle from_address(void*);
coroutine_handle();
coroutine_handle(std::nullptr_t);
coroutine_handle& operator=(std::nullptr_t);
};

代码做了简化。与想象的不同,这里没有直接调用协程状态的 resume_fndestroy_fn,实际上也无法调用它们,毕竟协程句柄只有一个承诺对象还拿不到协程状态。

不过这也不是什么不可逾越的难事,基于结构体成员__promise的相对位置做计算是行得通的,想想看offsetof这种设施就明白了,只是就有点依赖编译器生成的结构体成员尺寸和顺序了。为了屏蔽这些细节,clang 中doneresumedestroy是委托给 __builtin_coro_done__builtin_coro_resume__builtin_coro_destroy这些内置函数的,MSVC 中是委托给_coro_done_coro_resume_coro_destroy,看得出来,这些实现是编译器相关的,不具备可移植性。

网上搜了一下,没有找到相关源码来一窥究竟,不过开个脑洞想象一下:resumedestroy比较简单,一对一调用__rangeResume__rangeDestroy就好了;done难搞一些,可能需要判断状态机当前值__suspend_index,贴个协程状态的定义回忆一下:

struct __rangeFrame
{
void (*resume_fn)(__rangeFrame *);
void (*destroy_fn)(__rangeFrame *);
std::__coroutine_traits_impl<Generator>::promise_type __promise;
int __suspend_index;
bool __initial_await_suspend_called;
int from;
int to;
int i;
std::suspend_always __suspend_24_11;
std::suspend_always __suspend_26_9;
std::suspend_always __suspend_24_11_1;
};

__suspend_index 被初始化为0,每切换一次状态递增 1,所以只要这个值达到上限,就能说明整个协程结束了。

下面来看协程的核心__rangeResume是否和预测的一样:

/* This function invoked by coroutine_handle<>::resume() */
void __rangeResume(__rangeFrame * __f)
{

开幕雷击,这不就是个 duff device 吗,与 C++17 不同的是这里使用了 goto 分散代码

  try
{
/* Create a switch to get to the correct resume point */
switch(__f->__suspend_index) {
case 0: break;
case 1: goto __resume_range_1;
case 2: goto __resume_range_2;
case 3: goto __resume_range_3;
}

启动时挂起?是的话会发生第一次退出,本例中 promise_type::inistialze_suspend 返回的 suspend_always 会导致协程挂起,这个发生在 range 内部直调 __rangeResume 时

    /* co_await insights.cpp:24 */
__f->__suspend_24_11 = __f->__promise.initial_suspend();
if(!__f->__suspend_24_11.await_ready()) {
__f->__suspend_24_11.await_suspend(std::coroutine_handle<Generator::promise_type>::from_address(static_cast<void *>(__f)).operator std::coroutine_handle<void>());
__f->__suspend_index = 1;
__f->__initial_await_suspend_called = true;
return;
}

__resume_range_1 标签,这个发生在 while 循环中的 Generator::next 恢复协程运行时,注意循环中使用的变量已经放在了协程状态 _f 中,因此循环是无缝恢复的

    __resume_range_1:
__f->__suspend_24_11.await_resume();
for(__f->i = __f->from; __f->i <= __f->to; ++__f->i) {

co_yield 将调用 promise_type::yield_value 将生成结果保存在返回对象中,同时 yield_value 返回的 suspend_always 会导致协程挂起,从而让外部访问 value 内容。注意这个阶段 __suspend_index 一直保持 2 不变

      /* co_yield insights.cpp:26 */
__f->__suspend_26_9 = __f->__promise.yield_value(__f->i);
if(!__f->__suspend_26_9.await_ready()) {
__f->__suspend_26_9.await_suspend(std::coroutine_handle<Generator::promise_type>::from_address(static_cast<void *>(__f)).operator std::coroutine_handle<void>());
__f->__suspend_index = 2;
return;
}

__resume_range_2 标签,这个也发生在 while 循环中的 Generator::next 恢复协程运行时

      __resume_range_2:
__f->__suspend_26_9.await_resume();
} goto __final_suspend;

整个 for 循环期间抛出的任何异常都会被编译器捕获,并回调 promise_type::unhandled_exception 处理

  } catch(...) {
if(!__f->__initial_await_suspend_called) {
throw ;
} __f->__promise.unhandled_exception();
}

结束时挂起?是的话会发生最后一次退出,本例中 promise_type::final_suspend 给的 suspend_always 会导致协程挂起

  __final_suspend:

  /* co_await insights.cpp:24 */
__f->__suspend_24_11_1 = __f->__promise.final_suspend();
if(!__f->__suspend_24_11_1.await_ready()) {
__f->__suspend_24_11_1.await_suspend(std::coroutine_handle<Generator::promise_type>::from_address(static_cast<void *>(__f)).operator std::coroutine_handle<void>());
__f->__suspend_index = 3;
return;
}

__resume_range_3 标签,理论上没机会执行。若走到这里,会调用 __rangeDestroy 做一些清理工作

  __resume_range_3:
__f->destroy_fn(__f);
}

先补充承诺对象的两个接口定义:

        auto initial_suspend() { return std::suspend_always{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }

它们分别用于控制协程开始、结束前是否挂起,返回的类型称为等待对象,主要由三个接口组成:

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 {}
};

await_suspend & await_resume 用于穿插一些协程挂起前、恢复后的工作,一般配合 co_awaitco_yeild食用;上面列的这两个结构是特殊的等待对象,它们只实现了一个接口await_ready,用于指示是否挂起协程:std::suspend_always总是挂起,std::suspend_never总是不挂起;有了等待对象的铺垫,再来看看通读 __rangeResume的收获:

1) C++20 协程本质仍是个 duff device

2) 承诺对象的 initial_suspend 决定是否在初始化后挂起协程

3) 承诺对象的 final_suspend 决定是否在结束前挂起协程

4) 等待对象的 await_ready 表示异步结果是否就绪,若未就绪,需要挂起协程进行等待;否则继续协程

5) co_yield 对应承诺对象的 yield_value 接口,接口参数表示当前需要保存的数据

6) 协程挂起前会调用等待对象的 await_suspend,一般在这里发起异步调用

7) 协程恢复后会调用等待对象的 await_resume,一般在这里获取异步调用结果

8) 等待对象需要跨越协程的挂起状态进行访问,因此是放在协程状态中用于生命周期保持,这就是成员__suspend_xx_xx存在的底层逻辑

9) 协程挂起前会更新 __suspend_index,以便下次进入时在新位置执行代码

第 4) 点补充一个承诺对象的 yield_value 实现:

        auto yield_value(int value) {
current_value = value;
return std::suspend_always{};
}

生成值被保存到返回对象中,方便后续通过 value 接口访问:

    int value() { return handle.promise().current_value; }

以上就是协程 resume 的逻辑了,下面来看下 destroy

/* This function invoked by coroutine_handle<>::destroy() */
void __rangeDestroy(__rangeFrame * __f)
{
/* destroy all variables with dtors */
__f->~__rangeFrame();
/* Deallocating the coroutine frame */
/* Note: The actual argument to delete is __builtin_coro_frame with the promise as parameter */
operator delete(static_cast<void *>(__f), sizeof(__rangeFrame));
}

用于销毁协程状态。有两个点会走到协程销毁,一个是上面展示过的 __rangeResume末尾,一个是返回对象的析构:

    ~Generator() { if (handle) handle.destroy(); }

那到底是走哪个销毁的呢?增加一些输出看看:

class Generator {
struct promise_type {
~promise_type() { std::cout << "promise destroy" << std::endl; }
...
}
...
~Generator() { if (handle) { std::cout << "before destroy handle" << std::endl; handle.destroy(); std::cout << "after destroy handle" << std::endl; } }
}
1
2
3
4
5
before destroy handle
promise destroy
after destroy handle

承诺对象是协程状态的一个成员,它的析构意味着协程状态的销毁即__rangeDestroy 的调用,承诺对象析构的输出是包围在返回对象析构的输出中,这说明协程句柄的 destroy 直接调用了 __rangeDestroy,在 __rangeResume 末尾的自行销毁的代码__resume_range_3应该是没机会执行了,否则后面返回对象访问协程句柄时该崩溃了。

整个销毁顺序可以参考图上标的序号。

后记

把这个 demo 生成数列的过程通过表格完整模拟一遍:

语句 __suspend_index from to i __suspend_24_11 __suspend_26_9 __suspend_24_11_1 说明
auto gen = range(1,5) 0 -> 1 1 5 - suspend_always - - __resume_range_1 前退出
gen.next() 1 -> 2 1 5 1 suspend_always suspend_always - __resume_range_2 前退出
gen.next() 2 1 5 2 suspend_always suspend_always -  同上
gen.next() 2 1 5 3 suspend_always suspend_always -  同上
gen.next() 2 1 5 4 suspend_always suspend_always -  同上
gen.next() 2 1 5 5 suspend_always suspend_always -  同上
gen.next() 2 -> 3 1 5 6 suspend_always suspend_always suspend_always __resume_range_3 前退出,next 返回 false
gen.~Generator - - - - - - -  清理资源释放内存

while 循环共 5 次;实际调用 next 6 次,因为最后一次 next 时 done 返回了 false 导致 while 循环退出,没有打印 value 值;加上初始化进入 range 的一次,共 7 次,你看懂了吗。

不过表内有一点标红数据与实际有出入,即最后一次 value 仍为 5 而不是 6,这是测试代码:

#include <coroutine>
#include <iostream> struct Generator {
struct promise_type {
int current_value;
auto get_return_object() { return Generator{this}; }
auto initial_suspend() { return std::suspend_always{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
void unhandled_exception() {}
auto yield_value(int value) {
current_value = value;
return std::suspend_always{};
}
}; int n = 0;
std::coroutine_handle<promise_type> handle;
Generator(promise_type* p) : handle(std::coroutine_handle<promise_type>::from_promise(*p)) {}
~Generator() { if (handle) handle.destroy(); }
bool next() { return !handle.done() && (handle.resume(), n++, !handle.done()); }
int value() { return handle.promise().current_value; }
int N() { return n; }
}; Generator range(int from, int to) {
for (int i = from; i <= to; ++i) {
co_yield i;
}
} int main() {
auto gen = range(1, 5);
while (gen.next()) {
std::cout << gen.N() << ": " << gen.value() << std::endl;
} std::cout << gen.N() << ": " << gen.value() << std::endl;
}

和输出:

1: 1
2: 2
3: 3
4: 4
5: 5
6: 5

看起来像是编译器优化但我没有证据,感兴趣的读者可以研究汇编代码告诉我为什么。

总结

本文通过一个最基础的例子说明了 C++20 协程相关的概念:

* 协程体

* 协程状态

* 承诺对象

* 返回对象

* 协程句柄

和它们之间的关系:

另外通过两个在线工具,解析了编译器在底层所做的穿针引线工作,并阐释了 C++20 协程就是函数+状态机这一实质,并简单说明了接入 C++20 协程时用户需要实现的类型与接口,及其含义。

用户只需要提供一个返回对象、一个承诺对象,若干等待对象,就可以像写函数一样写协程啦!相比 C++17 需要自己维护变量的生命期,C++20 直接将协程所需参数和变量搬到协程状态中,为用户节省了大量的心智负担,使协程达到真正好用的水平。这样做的好处是变量存放于堆上,可大可小,不受固定栈尺寸限制,避免爆栈问题侵扰;缺点是当协程数量多时,动态内存分配频繁,容易引发内存碎片。

然而本例并不是一个能拿得出手的 C++20 协程例子,毕竟没有用户会手动 resume 协程!所以下一篇准备加入协程调度器,看看协程在真实场景中是怎么自己运转起来的,敬请期待~

参考

[1]. 深入浅出 C++ Lambda表达式:语法、特点和应用

[2]. C++ Lambda表达式的完整介绍

[3]. C++11:lambda表达式

[4]. C++11中的std::bind和std::function

[5]. 现代C++的回调技术--std::bind+std::function

[6]. std::function的原理以及实现

[7]. C++代码优雅之道:std::function使用全攻略(附性能优化技巧)

[8]. C++17结构化绑定

[9]. 【C++ 17 新特性 结构化绑定】深入理解C++ 17 结构化绑定[key, value] 的处理

[10]. C++ 17 结构化绑定

[11]. 掌握 C++17:结构化绑定与拷贝消除的妙用

[12]. c++17之std::optional,std::variant以及std::any

[13]. 如何优雅的使用 std::variant 与 std::optional

[14]. 17. 使用std::optional和std::variant进行重构

[15]. 理解C++折叠表达式(Fold Expression)

[16]. C++ 折叠表达式:优雅处理可变参数模板

[17]. 【C++ 17 新特性 折叠表达式 fold expressions】理解学习 C++ 17 折叠表达式 的用法

[18]. C++中的聚合初始化

[19]. C++20 中使用括号进行聚合初始化:新特性与实践指南

[20]. 漫谈C++类型擦除(Type Erasure)

[21]. C++语法糖(coroutine)详解以及示例代码

[22]. 【并发编程二十一:终章】c++20协程( co_yield、co_return、co_await )

[23]. 从无栈协程到C++异步框架

[24]. 从无栈协程到C++异步框架—多线程环境下的协程调度

[25]. gcc里的coroutine_handle

[26]. 21. C++快速入门--协程 Coroutine 入门

[27]. Exploring the C++ Coroutine

[28]. C++那些事之C++20协程

[29]. C++协程小记-2

[30]. 浅谈C++20 协程那点事儿

[31]. 一篇文章搞懂 C++ 20 协程 Coroutine

[32]. C++ 20 协程 Coroutine(3,剖析)

[33]. C++20 Coroutines: operator co_await

[34]. [翻译] 为什么 C++20 是最 awesome 的网络编程语言

[35]. 从HelloWold开始,深入浅出C++ 20 Coroutine TS

协程本质是函数加状态机——零基础深入浅出 C++20 协程的更多相关文章

  1. Android零基础入门第20节:CheckBox和RadioButton使用大全

    原文:Android零基础入门第20节:CheckBox和RadioButton使用大全 本期先来学习Button的两个子控件,无论是单选还是复选,在实际开发中都是使用的较多的控件,相信通过本期的学习 ...

  2. 函数:内嵌函数和闭包 - 零基础入门学习Python020

    函数:内嵌函数和闭包 让编程改变世界 Change the world by program 内嵌函数和闭包 接下来这两节课我们谈的话题可能会"比较高级",所以如果是零基础的朋友, ...

  3. 函数:lambda表达式 - 零基础入门学习Python021

    函数:lambda表达式 让编程改变世界 Change the world by program lambda表达式 Python允许使用lambda关键字来创建匿名函数.我们提到一个新的关键字:匿名 ...

  4. Android零基础入门第23节:ImageButton和ZoomButton使用大全

    原文:Android零基础入门第23节:ImageButton和ZoomButton使用大全 上一期我们学习了ImageView的使用,那么本期来学习ImageView的两个子控件ImageButto ...

  5. Android零基础入门第54节:视图切换组件ViewSwitcher

    原文:Android零基础入门第54节:视图切换组件ViewSwitcher 前面三期学习了ProgressBar系列组件,那本期开始一起来学习ViewAnimator组件. 一.ViewAnimat ...

  6. Android零基础入门第40节:自定义ArrayAdapter

    原文:Android零基础入门第40节:自定义ArrayAdapter ListView用起来还是比较简单的,也是Android应用程序中最重要的一个组件,但其他ListView可以随你所愿,能够完成 ...

  7. Android零基础入门第79节:Intent 属性详解(上)

    Android应用将会根据Intent来启动指定组件,至于到底启动哪个组件,则取决于Intent的各属性.本期将详细介绍Intent的各属性值,以及 Android如何根据不同属性值来启动相应的组件. ...

  8. Android零基础入门第29节:善用TableLayout表格布局,事半功倍

    原文:Android零基础入门第29节:善用TableLayout表格布局,事半功倍 前面学习了线性布局和相对布局,线性布局虽然方便,但如果遇到控件需要排列整齐的情况就很难达到要求,用相对布局又比较麻 ...

  9. Android零基础入门第24节:自定义View简单使用

    原文:Android零基础入门第24节:自定义View简单使用 当我们开发中遇到Android原生的组件无法满足需求时,这时候就应该自定义View来满足这些特殊的组件需求. 一.概述 很多初入Andr ...

  10. Android零基础入门第22节:ImageView的属性和方法大全

    原文:Android零基础入门第22节:ImageView的属性和方法大全 通过前面几期的学习,TextView控件及其子控件基本学习完成,可以在Android屏幕上显示一些文字或者按钮,那么从本期开 ...

随机推荐

  1. ORB算法介绍 Introduction to ORB (Oriented FAST and Rotated BRIEF)

    Introduction to ORB (Oriented FAST and Rotated BRIEF) 1. Introduction ORB(Oriented FAST and Rotated ...

  2. 开源姿势识别 Demo

    最近项目中要用到姿势识别,调研了 BlazePose. MoveNet 等模型,以下是一些详细的对比. 包括 mediapipe 和 tfjs 等运行环境,webgl,webgpu,wasm 都做了尝 ...

  3. Python 潮流周刊#95:像人类一样使用计算机(摘要)

    本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章.教程.开源项目.软件工具.播客和视频.热门话题等内容.愿景:帮助所有读者精进 Python 技术,并增长职 ...

  4. 2025年BI工具趋势:DataFocus与FineBI的技术创新对比

    1. 摘要 DataFocus 和 FineBI 都是旨在帮助企业利用数据进行决策的商业智能 (BI) 产品.DataFocus 强调其下一代.基于搜索的 BI 方法,侧重于易用性和快速仪表板创建,尤 ...

  5. MySQL建立RFM模型

    1.数据来源 charge_record表数据 提取代码如下: # coding=utf-8import pymysql# 原数据库链接db1 = pymysql.connect( host='*** ...

  6. eolinker响应预处理:返回结果内循环读取同类数据,设置为变量

    特别注意:需要使用全局变量或者预处理前务必阅读本链接https://www.cnblogs.com/becks/p/13713278.html 场景描述: 删除(清空)购物车接口,需要传入获取的每一项 ...

  7. ZeroTier 穿透ZeroNSD Quickstart

    Create a ZeroTier Network# You may do this manually through the ZeroTier Central WebUI, Install Zero ...

  8. Sentinel——系统规则(系统自适应保护)

    目录 系统自适应保护 系统规则 系统自适应保护 Sentinel 系统自适应保护从整体维度对应用入口流量进行控制,结合应用的 Load.总体平均 RT.入口 QPS 和线程数等几个维度的监控指标,让系 ...

  9. SpringBoot事件驱动开发

    应用启动过程生命周期事件感知(9大事件).应用运行中事件感知(无数种) 事件发布:ApplicationEventPublisherAware或注入:ApplicationEventMulticast ...

  10. sql注入与防止sql注入

    数据库中的数据 sql代码 package com.zjw.jdbc2; import java.sql.Connection; import java.sql.DriverManager; impo ...