首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
247 阅读
2
2018总结与2019规划
137 阅读
3
nvim番外之将配置的插件管理器更新为lazy
136 阅读
4
从零开始配置 vim(15)——状态栏配置
127 阅读
5
PDF标准详解(五)——图形状态
106 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
emacs
VimScript
读书笔记
linux
elisp
文本编辑器
Java
投资理财
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
368
篇文章
累计收到
32
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
1
篇与
的结果
2026-05-25
C++ 20 协程的探索
C++ 20 中新增的协程特性可以说是近几年更新的最重要的一个特性。早在我使用python的时候,我就体会到python中的协程和它在处理list时使用yield 的优雅。现在C++ 也支持了协程,我们可以实现类似python中的功能了完整的协程例子在python中,我们可以实现如下的函数def fibonacci(n): a, b = 0, 1 for _ in range(n): yield a # 暂停函数,返回a a, b = b, a + b # 使用生成器 for num in fibonacci(5): print(num)早期的C++ 中我们只能使用std::vector<int> fibonacci(int n) { vector<int> res; int a = 0, b = 1; for(int i = 0; i < n; i++) { int c = a + b; a = b; b = c; res.push_back(c); } } for(auto i : fibonacci(5)) std::cout << i << std::endl;上面两个对比,python示例的特点在于它不是每次调用都计算指定范围内的所有结果,而是每调用一次接着上一次的进行调用,能极大的提升运行效率。一个好的消息是,在C++20 中新增了协程,我们也能实现类似的功能。而坏消息就是C++20 的协程设计成了底层的“语言特性骨架”,没有提供开箱即用的高层库,所以它的概念相对复杂。#include <coroutine> struct Generator { struct promise_type { int currentValue; std::suspend_always initial_suspend() { return std::suspend_always{}; } std::suspend_always final_suspend() noexcept { return std::suspend_always{}; } auto yield_value(int value) { currentValue = value; return std::suspend_always{}; } Generator get_return_object() { return Generator{ std::coroutine_handle<promise_type>::from_promise(*this) }; } void return_void() {} void unhandled_exception() { std::terminate(); } }; std::coroutine_handle<promise_type> h; Generator(std::coroutine_handle<promise_type> handle) : h(handle) {} ~Generator() { if (h) h.destroy(); } bool move_next() { if (!h || h.done()) return false; h.resume(); return !h.done(); } int get_value() { return h.promise().currentValue; } }; Generator fibonacci(int n) { int a = 0; int b = 1; int i = 0; while (i < n) { co_yield a; int next = a + b; a = b; b = next; i++; } } int _tmain(int argc, TCHAR* argv[]) { Generator generator = fibonacci(10); while (generator.move_next()) { std::cout << generator.get_value() << std::endl; } return 0; }比起python的版本,它确实复杂了不少。但是我们可以从逻辑上一一拆解协程实现原理首先我们知道,普通的函数是通过栈来存储数据的,在调用时首先经过参数压栈,然后在栈中初始化函数的局部变量,接着执行函数代码。在函数退出时要在接收返回值的内存处初始化返回值对象,最后执行出栈操作清理整个函数栈。协程函数是可以随时挂起并回退到调用处,或者由外部直接恢复执行的,所以在回到调用位置时并没有执行函数栈的清理操作,而且需要保存函数当前执行的上下文环境,在需要的时候进行恢复。C++的协程采用的是无栈的设计,也就是说协程函数本身并没有产生栈,它的所有信息由编译器生成处理。到这里我们知道针对每个协程,编译器会生成处理它的代码包括保存上下文环境,真正执行挂起和恢复。编译器中通过coroutine_handle 对象来管理每个协程对象。我们无需知道编译器具体是如何实现管理协程的,我们只需要获取它的对象,并调用对象的方法就能操作对象。这也是面向对象的体现。我们知道编译器是通过生成一个协程对象来管理协程的。那么它是什么时候生成这个对象的呢?答案是它在发现某个函数为协程函数时会生成一个协程对象,对象生成的时机早于协程函数开始执行的时候,主要是因为它需要接管协程函数的栈并保存上下文环境。那么编译器是如何知道某个函数是协程函数的呢?主要是通过识别里面是否有 co_await、co_yield、co_return 这么几个关键字。到此我们先梳理一下,首先编译器在编译时扫描到了函数中有 co_await、co_yield、co_return 这几个关键字,因此它会生成协程对象接管函数栈和上下文,并且不会为函数本身生成栈帧。对于协程函数,为什么不能直接返回具体的返回值类型,而需要利用结构体包装一层呢?例如对于 fibonacci,我们知道它实际返回的是int类型,为什么不能直接返回int 呢?如果我们直接返回int会得到这样的错误:error C2039: "promise_type": 不是 "std::coroutine_traits<int,int>" 的成员也就是说,协程的类型已经定义好了,它必须是 std::coroutine_traits<T> 的类型,我们可以在标准库中看到,这个类型的定定义如下:template <class _Ret, class = void> struct _Coroutine_traits {}; template <class _Ret> struct _Coroutine_traits<_Ret, void_t<typename _Ret::promise_type>> { using promise_type = typename _Ret::promise_type; }; _EXPORT_STD template <class _Ret, class...> struct coroutine_traits : _Coroutine_traits<_Ret> {};从上面可以看到,返回值必须要包含一个名为 promise_type 的结构。这个结构实际上是一个接口,在编译器帮我们管理协程,实现协程的创建、挂起、恢复、结束时它给我们提供了一系列的接口,方便我们在这些事件发生时执行一些自定义的代码。上面我们实现了这么两个接口:initial_suspend: 协程对象被创建时执行final_suspend: 协程对象被销毁时执行这两个函数的返回值也是一个接口,需要返回一个 awaiter(等待体)。这个 awaiter 我们也可以将它看作一个接口。它主要有三个函数需要实现:bool await_ready(): 如果返回 true,则表示已经就绪,无需挂起;否则表示需要挂起await_suspend(coroutine_handle<>) 当协程被挂起时,协程对象会保存当前协程的对象,此时 await_suspend 会被调用await_resume() 协程恢复执行时,该函数被调用需要注意,上面三个函数除了 await_ready 外,另外两个都没有给出返回值类型,标准并没有规定它们的返回值类型。这里我用了标准库实现的简单的等待体 std::suspend_always,它的实现如下:_EXPORT_STD struct suspend_always { _NODISCARD constexpr bool await_ready() const noexcept { return false; } constexpr void await_suspend(coroutine_handle<>) const noexcept {} constexpr void await_resume() const noexcept {} };还有一个类似的实现_EXPORT_STD struct suspend_never { _NODISCARD constexpr bool await_ready() const noexcept { return true; } constexpr void await_suspend(coroutine_handle<>) const noexcept {} constexpr void await_resume() const noexcept {} };到此为止,我们知道了4个接口,分别是协程对象被创建和销毁时调用,协程被挂起和恢复时调用。到这里,读者可能有疑问,这些接口为什么不设计到promise_type 里面,而非要放到两个结构里面,非要凭空多出一个概念呢?是不是这样更难理解,显得逼格更高呢?原因与难度和逼格无关,这是因为等待体主要是用来控制协程是挂起还是执行,以前我陷入了一个误区,我以前认为每次调用co_yield、co_await 一定会陷入等待。但是实际上它们只是操作等待体,与协程实际进行等待还是继续执行没有关系。我们是通过具体的等待体来实现此时协程是该继续执行还是等待。等待体与协程本身的控制应该分开,协程根据等待体的返回值来决定该等待还是继续执行。到这里我们再总结一次:编译器扫描到了协程函数,会定义一个协程对象来管理协程协程对象创建时执行 initial_suspend 函数,对象根据函数的返回值来决定是否要执行协程函数协程函数被执行时会依次执行,直到遇到 co_yield、co_await、co_return遇到上面的关键字之后它通过关键字操作的等待体中的 await_ready 的返回值来决定这个时候要不要接着执行协程如果需要挂起协程,那么它会调用等待体中的 await_suspend如果在外部恢复了协程,此时会调用等待体中的 await_resume如果协程函数结束了,会调用 final_suspend有读者可能会问了,你说 co_yield、co_await、co_return,我在上面的例子中也没看到有等待体呀。这是因为编译器对我们 co_yield a; 这个语句中执行了类型转换,将int转化为了等待体。我们在实现这个转化上有两种方式,第一个就是我自己定义一个等待体,然后通过构造函数实现int到等待体的转化,第二个就是实现接口, 对于co_yield 来说这个接口是一个名为 yield_value 的函数。编辑器在执行类型转化时,会进行下面的流程:如果在 promise_type 中定义了对应的转换函数,那么会调用转换函数来进行转换否则在全局查找能否有办法将后面的数据类型转化为一个等待体这里我没有自定义等待体,就采用最简单的 yeild_value了。对于 co_await 来说,可以通过 await_transform 函数来进行转化在上面的例子中,还有三个函数没有介绍到:get_return_objectreturn_voidunhandled_exception首先是 get_return_object,从名字上看,它是用来获取返回值对象的,我们是要用它来获取哪个返回值对象呢?是协程函数的吗?当然不是,这个函数是在协程对象自身被创建之后会调用,也就是说这里的返回对象其实是协程对象。return_void / return_value函数,这个函数是协程体结束之后如果协程函数自身返回时被调用,对于第一个函数,它调用的时机是在协程函数结束之前,而return_value 则是在调用 co_return 返回一个值。它们与 co_yield 类似,外部都是通过这种机制来实现获取协程执行到此的一些值,但是调用 return_void 和 return_value 之后意味着协程函数已经执行完毕,没有机会再恢复了。最后一个函数是:unhandled_exception, 这个函数表示,如果协程函数中发生异常,此函数将被调用。总结到此我们根据上述代码来总结一下协程的执行流程:编译器扫描到了协程函数,会定义一个协程管理对象来管理协程协程管理对象被创建之后,执行 get_return_object 此时外界有机会获取到管理对象,此时我们将handle 对象保存到了 Generator 对象中协程管理对象执行 initial_suspend 函数,对象根据函数的返回值来决定是否要执行协程函数。这里我们通过返回suspend_always 来将协程挂起外界通过执行 move_next 调用里面的 h.resume() 来恢复协程的执行,协程会按顺序执行直到遇到 co_yield、co_await、co_return这里遇到了关键字 co_yield 它会调用promise_type 中的 yield_value 执行整形数据到awaiter 的转化,这里返回suspend_always 来将协程挂起此时协程被挂起,那么它会调用等待体中的 await_suspend如果在下一次循环中又一次调用 move_next 恢复了协程,此时会调用等待体中的 await_resume就这样每调用一次 move_next 就执行一次计算,获取数列中的下一个数字循环结束,协程函数执行到函数体尾部或者执行了 co_return ,会调用 return_value或者 return_void 让外界有机会获取到协程函数本身的返回值,这个函数本身不返回值,所以这里会调用 return_void。协程函数结束了,执行 final_suspend,此时可以做一些清理工作。这里我们返回 suspend_always 来暂停协程,主要是为了协程结束之后,仍然可以获取协程计算的结果。主函数结束,Generator 对象被销毁,此时调用析构函数中的 h.destroy() 销毁协程管理对象
2026年05月25日
2 阅读
0 评论
0 点赞