当前位置: 首页 > news >正文

协程本质是函数加状态机——零基础深入浅出 C++20 协程

非对称表示协程控制权的转移是单向的,即通过 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 底层是如何实现的?循环变量是如何恢复的?带着这些疑问,有请 C++ Insights 上场,看看这个 demo 的原形 (注意开启Show coroutine transformation选项):

查看代码

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

http://www.jsqmd.com/news/1112941/

相关文章:

  • Super IO:Blender剪贴板导入导出插件终极指南,3倍提升3D工作流效率
  • 微信公众号授权登录全流程实战:从OpenID到JWT Token的完整实现
  • 2026佳木斯黄金回收白银回收铂金回收旧料回收怎么选?五家高实价铂金白银线下门店测评清单 + 联系方式
  • C++工程化开发规范、内存泄漏排查、常见报错与高阶实战总结
  • AutoScreenshot深度解析:跨平台自动截图工具的高效应用指南
  • Spring Boot与AI集成开发实战指南
  • 2026年AI网站开发公司排名,高端定制服务商榜单
  • P1395 会议【洛谷算法习题】
  • 【深度学习】OpenCV 人脸识别实战:LBPH 算法实现简单人脸识别
  • C++入门基石:语言定位、编译流程与基础语法深度解析
  • 机器学习问题定义:从模糊需求到可执行任务的实战方法论
  • 机器学习三要素与核心算法实战指南
  • 20种AI Agent架构实战解析:从基础到高级方案
  • 室内渲染进阶指南:从平淡无奇到照片级效果的6个核心法则
  • 【2026运营版】B2B2C多商户外贸电商系统|跨境商城|云仓库代发+分销+佣金+POS下单
  • 实习生转正复盘:技术成长要有证据,不要只靠感觉努力
  • 字节跳动 data 系统后台开发面经:一面项目和智能指针打底,二面直接补 Linux、HTTP 和逻辑题
  • C++智能指针全面精讲:auto_ptr、unique_ptr、shared_ptr、weak_ptr原理与实战
  • Winform加密算法
  • 2026年7月亲测:深圳高空吊装企业性价比分享
  • Uniapp上架苹果4.3a被拒?我摸出了躺过的万能公式!
  • 惠州儿童牙科医院选择指南
  • 鸿蒙原生 ArkTS 自定义布局深度解析:onMeasure / onLayout 实战
  • Koji Build 命令参数深度解析:从入门到精通
  • 2026年,苦荞快餐粉引领健康新潮流
  • 如何优雅地下载文档:kill-doc浏览器脚本使用指南
  • Matt Pocock Skills 安装与上手指南:让 AI 编程从“能跑“到“靠谱“
  • 116、asyncio 异步编程(二):Task、Future、gather、create_task 并发模式
  • CryptoHack「Hex」解题思路:从十六进制到Flag
  • 勇士传说学习心得