漳州做网站制作,网站建设与维护大作业,广州室内设计公司排名榜,wordpress访问次数插件摘要#xff1a;C20 引入的协程机制为异步编程提供了轻量级解决方案#xff0c;其核心优势在于通过用户态调度实现高效的上下文切换#xff0c;适用于 I/O 密集型任务、生成器模式等场景。本文系统阐述 C20 协程的底层原理与实践要点#xff0c;首先解析协程的基本结构C20 引入的协程机制为异步编程提供了轻量级解决方案其核心优势在于通过用户态调度实现高效的上下文切换适用于 I/O 密集型任务、生成器模式等场景。本文系统阐述 C20 协程的底层原理与实践要点首先解析协程的基本结构包括promise_type状态控制器、coroutine_handle句柄及co_await、co_yield、co_return关键字的工作机制揭示协程启动、暂停、恢复与终止的完整生命周期。其次深入探讨协程的实现细节如 Awaitable 与 Awaiter 对象的转换逻辑、协程帧的内存管理分配与释放、编译器对协程的状态机转换基于暂停点索引的执行控制。针对协程使用中的关键问题分析内存分配优化策略自定义分配器与内存池、协程与线程的本质区别用户态调度 vs 内核态调度以及对称转移导致的栈溢出风险及解决方案尾调用优化与std::noop_coroutine。本文通过实例与伪代码还原协程的编译器转换过程为开发者理解协程机制、规避常见问题提供理论与实践参考。
关键词C20协程异步编程promise_type上下文切换
1 协程简介 C20 引入了协程Coroutines支持。协程一种能够暂停和恢复执行的特殊函数非常适合处理异步操作、生成器、状态机等场景相比于通过回调函数、线程等方式协程能够更清晰地组织代码减少上下文切换的开销提高代码的可读性和可维护性。
1.1 协程和函数 在 C 中协程是一种特殊的函数调用它与普通函数存在显著区别。普通函数一旦开始执行会一直运行到返回语句或函数结束期间无法暂停而协程在执行过程中可以在特定点暂停保存当前的执行状态包括局部变量、指令指针等之后在合适的时机可以从暂停点恢复执行继续完成后续操作。 从执行流程来看普通函数的调用是一种栈式的调用方式遵循 “先进后出” 的原则每次调用都会在调用栈上分配新的栈帧协程则拥有自己独立的状态存储其暂停和恢复不依赖于传统的调用栈这使得协程的上下文切换成本远低于线程切换。 简单回顾下普通函数的调用流程一个普通的函数调用流程如下
函数调用(call)程序将控制权转移给被调用函数同时将返回地址压入调用栈。函数执行(execute)被调用函数开始执行它会使用栈帧来存储局部变量、参数、返回值等。函数返回(return)被调用函数执行完毕将返回值压入栈帧然后将控制权返回给调用函数。调用栈弹出(pop)调用函数从调用栈中弹出返回地址恢复控制权。 其中函数调用时会保存当前的执行状态包括局部变量、指令指针等并将控制权交给被调用函数。当被调用函数执行完毕后会从调用栈中弹出返回地址恢复调用函数的执行状态继续执行后续操作。也就是说函数一旦被调用就没有回头路直到函数调用结束才会将控制权还给调用函数。 而相比之下协程的调用路程要复杂的多
协程启动start当协程被调用时系统会为其分配独立的状态存储不同于传统栈帧用于保存局部变量、指令指针、寄存器状态等信息。此时协程进入初始状态可根据其返回类型的initial_suspend策略决定是否立即执行或暂停。协程执行execute协程开始执行与普通函数类似但在遇到co_await、co_yield等关键字时会触发暂停逻辑。 若执行co_await expr协程会先计算表达式expr得到一个 “等待体”awaitable然后调用该等待体的awit_suspend方法。若该方法返回true或一个协程句柄当前协程会暂停将控制权交还给调用者或切换到其他协程若返回false则继续执行。若执行co_yield expr本质是通过特殊的等待体暂停协程并向调用者返回一个值之后可从暂停点恢复。 协程暂停suspend协程暂停时其当前执行状态被完整保存到独立存储中调用栈不会被销毁。此时控制权返回给调用者或调度器调用者可继续执行其他任务或在合适时机恢复协程。协程恢复resume调用者通过协程句柄的resume()方法触发协程恢复。系统从保存的状态中还原执行环境协程从暂停点继续执行后续代码。协程结束finalize当协程执行到co_return语句或函数末尾时会触发final_suspend策略。通常此时协程会进入最终暂停状态等待调用者通过句柄销毁其状态存储以释放资源。 这种流程使得协程能够在执行过程中多次暂停和恢复且每次切换仅涉及状态存储的读写无需像线程那样切换内核态上下文因此效率更高。同时协程的状态保持特性让异步操作的代码编写更接近同步逻辑大幅降低了回调嵌套带来的复杂性。
1.2 简单的协程 下面是一个简单的 C20 协程示例展示了协程的基本使用方式
#include iostream
#include coroutine
#include thread
#include chrono
#include spdlog/spdlog.hstruct SimpleCorontinePromise;
struct SimpleCorontine {using promise_type SimpleCorontinePromise;
};struct SimpleCorontinePromise {SimpleCorontine get_return_object() {SPDLOG_INFO(get_return_object);return {};}void return_void() {SPDLOG_INFO(return_void);}std::suspend_never initial_suspend() noexcept {SPDLOG_INFO(initial_suspend);return {};}std::suspend_never final_suspend() noexcept {SPDLOG_INFO(final_suspend);return {};}void unhandled_exception() {SPDLOG_INFO(unhandled_exception);}
};SimpleCorontine MySimpleCorontine() {SPDLOG_INFO(Corontine Start);co_return;SPDLOG_INFO(Corontine End);
}int testSimpleCorontine() {SPDLOG_INFO(Main thread started executing 1);auto coro MySimpleCorontine(); // Ensure the lifecycle of coro is managedSPDLOG_INFO(Main thread started executing 2);return 0;
}上面的代码定义了一个简单的协程MySimpleCorontine其包括
协程函数MySimpleCorontine用于定义协程的执行逻辑。协程Promise类型SimpleCorontinePromise用于定义协程的状态管理和返回值处理。Promise必须 定义get_return_object()方法用于返回协程对象。定义return_void()方法用于处理协程函数执行完毕后的逻辑。定义initial_suspend()方法用于定义协程的初始暂停策略。定义final_suspend()方法用于定义协程的最终暂停策略。定义unhandled_exception()方法用于处理协程执行过程中发生的异常。 协程类型SimpleCorontine用于表示协程对象。而SimpleCorontine必须定义promise_type成员用于指定Promise类型。 在协程的第一行调用get_return_object是为了确保返回对象的有效性、管理状态和资源、提供异常安全性、简化控制流以及满足编译器设计的需要。 其执行结果如下
[2025-07-23 21:04:45.247] [info] [SimpleCorontine.cpp:44] Main thread started executing 1
[2025-07-23 21:04:45.247] [info] [SimpleCorontine.cpp:14] get_return_object
[2025-07-23 21:04:45.247] [info] [SimpleCorontine.cpp:23] initial_suspend
[2025-07-23 21:04:45.247] [info] [SimpleCorontine.cpp:38] Corontine Start
[2025-07-23 21:04:45.248] [info] [SimpleCorontine.cpp:19] return_void
[2025-07-23 21:04:45.248] [info] [SimpleCorontine.cpp:28] final_suspend
[2025-07-23 21:04:45.248] [info] [SimpleCorontine.cpp:47] Main thread started executing 2从执行结果我们可以整理出其基本的调用流程能够注意到co_return之后的代码不会被执行这是因为co_return会触发final_suspend策略导致协程进入最终暂停状态。
1.3 协程resume 上面的代码中我们并没有控制协程的调用流程似乎协程只是按照某种约定按照顺序调用规定的函数虽然事实也是如此。我们尝试将代码修改成下面的样子通过resume来控制协程的调用流程。改动如下完整的代码见
#include iostream
#include coroutine
#include thread
#include chrono
#include spdlog/spdlog.hstruct SimpleCoroutinePromise;struct SimpleCoroutine {using promise_type SimpleCoroutinePromise;std::coroutine_handlepromise_type handle;SimpleCoroutine(std::coroutine_handlepromise_type handle) : handle(handle) {}SimpleCoroutine(const SimpleCoroutine) delete;SimpleCoroutine operator(const SimpleCoroutine) delete;SimpleCoroutine(SimpleCoroutine other) noexcept : handle(other.handle) {other.handle nullptr;}SimpleCoroutine operator(SimpleCoroutine other) noexcept {if (this ! other) {if (handle) {handle.destroy(); // Destroy the current handle if it exists}handle other.handle;other.handle nullptr;}return *this;}void resume() {if (handle) {handle.resume();}}~SimpleCoroutine() {if (handle) {handle.destroy();}}
};struct SimpleCoroutinePromise {SimpleCoroutine get_return_object() {SPDLOG_INFO(get_return_object);return SimpleCoroutine(std::coroutine_handleSimpleCoroutinePromise::from_promise(*this));}void return_void() {SPDLOG_INFO(return_void);}std::suspend_always initial_suspend() noexcept {SPDLOG_INFO(initial_suspend);return {};}std::suspend_always final_suspend() noexcept {SPDLOG_INFO(final_suspend);return {};}void unhandled_exception() {SPDLOG_INFO(unhandled_exception);}
};SimpleCoroutine MySimpleCoroutine() {SPDLOG_INFO(Coroutine Start);co_return; // This will directly return, and the coroutine ends hereSPDLOG_INFO(Coroutine End); // This line will not be executed
}int testSimpleCorontine() {SPDLOG_INFO(Main thread started executing 1);auto coro MySimpleCoroutine();SPDLOG_INFO(Main thread started executing 2);coro.resume();SPDLOG_INFO(Main thread started executing 3);return 0;
}上面的代码输出结果为
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:78] Main thread started executing 1
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:48] get_return_object
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:57] initial_suspend
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:80] Main thread started executing 2
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:72] Coroutine Start
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:53] return_void
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:62] final_suspend
[2025-07-23 22:31:34.783] [info] [SimpleCorontine.cpp:82] Main thread started executing 3上面的输出相比之前的输出能够发现执行完协程的初始化相关函数之后协程就讲控制权交给了主函数主函数通过resume来恢复协程的执行之后再执行协程相关的代码。相关的改动
创建一个 handle 来管理协程的生命周期。这个 handle 允许我们控制何时恢复协程的执行以及最终何时销毁它。修改suspend状态其中: suspend_always协程在达到指定的挂起点如initial_suspend()时会暂时停止执行并将控制权返回给调用者。调用者随后可以选择何时恢复协程的执行。suspend_never协程在到达挂起点时直接继续执行控制权不会返回给调用者。这意味着协程会在达到终点后直接终止而不会暂停。
1.4 协程yield 除了基本的结构协程还有其他的功能比如yield。co_yield是 C20 协程中用于 “产出值并暂停” 的关键字主要用于实现生成器Generator模式允许协程在执行过程中多次返回值每次返回后暂停等待下次被恢复时继续执行。co_yield本质上是一种语法糖等价于co_await promise.yield_value(expr)。其工作流程如下
当协程执行到co_yield expr 时首先计算表达式expr的值将该值存储到promise_type中供调用者获取协程暂停执行将控制权交还给调用者当调用者通过协程句柄恢复协程时协程从 co_yield 之后的代码继续执行。 基于此我们可以实现一个简单的generator用来生成数字。
struct Generator {struct GeneratorPromise {using Handle std::coroutine_handleGeneratorPromise;Generator get_return_object() {return Generator(Handle::from_promise(*this));}std::suspend_always initial_suspend() noexcept {return {};}std::suspend_always final_suspend() noexcept {return {};}void return_void() {}void unhandled_exception() {}std::suspend_always yield_value(int v) {value v;return {};}int value{};};using promise_type GeneratorPromise;using Handle std::coroutine_handlepromise_type;Generator(Handle handle) : handle(handle) {}~Generator() {if (handle) {handle.destroy();}}Generator(const Generator) delete;Generator operator(const Generator) delete;Generator(Generator other) noexcept : handle(other.handle) {other.handle nullptr;}Generator operator(Generator other) noexcept {if (this ! other) {if (handle) {handle.destroy();}handle other.handle;other.handle nullptr;}return *this;}bool done(){return handle.done();}int next(){if(done()){return - 1;}handle.resume();if (done()) {return - 1;}return handle.promise().value;}Handle handle;
};Generator GeneratorNum(){for(int i 0;i 5;i ){co_yield i;}
}int testSimpleGenerator() {auto gen GeneratorNum();while(!gen.done()){SPDLOG_INFO(num {}, gen.next());}return 0;
}代码的输出为
[2025-07-23 22:50:28.006] [info] [SimpleCorontine.cpp:165] num 0
[2025-07-23 22:50:28.007] [info] [SimpleCorontine.cpp:165] num 1
[2025-07-23 22:50:28.007] [info] [SimpleCorontine.cpp:165] num 2
[2025-07-23 22:50:28.007] [info] [SimpleCorontine.cpp:165] num 3
[2025-07-23 22:50:28.007] [info] [SimpleCorontine.cpp:165] num 4
[2025-07-23 22:50:28.007] [info] [SimpleCorontine.cpp:165] num -11.5 协程co_return co_return 是用于终止协程执行并返回结果的关键字类似于普通函数中的 return但专门针对协程的特性设计用于结束协程的生命周期并传递最终结果。co_return 的语法有两种形式
无返回值co_return。用于不需要返回最终结果的协程仅表示协程执行结束。带返回值co_return 表达式。用于需要向调用者返回最终结果的协程表达式的值会被传递给协程的promise_typepromise_type。 当协程执行到co_return时会触发以下流程
处理返回值 若为co_return expr;则表达式 expr的值会被传递给协程promise_type的 return_value(expr)方法需在 promise_type中定义由promise_type存储该结果供调用者获取。若为 co_return;则调用promise_type的 return_void()方法无返回值场景。 触发最终挂起 协程执行完返回值处理后会调用promise_type的 final_suspend()方法根据其返回的挂起策略std::suspend_always或 std::suspend_never决定是否暂停。通常会返回 std::suspend_always让协程进入最终暂停状态等待调用者通过协程句柄销毁资源。 协程终止协程进入最终暂停状态后其生命周期并未完全结束需等待调用者显式调用coroutine_handle::destroy()释放协程占用的资源如状态存储、局部变量等。 之前的协程例子是不带返回值的这里通过reutrn_value来处理返回值。
// 带返回值的协程返回类型
struct ResultTask {struct promise_type {std::string result; // 存储协程的返回结果// 获取协程返回对象ResultTask get_return_object() {return ResultTask{std::coroutine_handlepromise_type::from_promise(*this)};}// 初始挂起立即执行std::suspend_never initial_suspend() { return {}; }// 最终挂起暂停以等待销毁std::suspend_always final_suspend() noexcept { return {}; }// 处理带值的 co_returnvoid return_value(const std::string val) {result val; // 保存返回值}// 异常处理void unhandled_exception() { std::terminate(); }};std::coroutine_handlepromise_type handle;// 提供接口让调用者获取返回值std::string get_result() const {return handle.promise().result;}
};// 示例协程执行一些操作后返回结果
ResultTask process_data(int input) {SPDLOG_INFO(process_data);SPDLOG_INFO(input {}, input);// 模拟一些处理逻辑if (input 0) {co_return error, the input is negative; // 返回错误信息}int result input * 2;co_return process data done: std::to_string(result); // 返回计算结果
}int testResultTask() {// 启动协程auto task process_data(10);SPDLOG_INFO(task handle done {}, task.handle.done());// 检查协程是否完成if (task.handle.done()) {SPDLOG_INFO(task handle done {}, task.handle.done());SPDLOG_INFO(task result {}, task.get_result());}// 释放协程资源task.handle.destroy();return 0;
}上述代码的输出结果是:
[2025-07-24 08:47:39.055] [info] [SimpleCorontine.cpp:209] process_data
[2025-07-24 08:47:39.055] [info] [SimpleCorontine.cpp:210] input 10
[2025-07-24 08:47:39.055] [info] [SimpleCorontine.cpp:224] task handle done true
[2025-07-24 08:47:39.055] [info] [SimpleCorontine.cpp:228] task handle done true
[2025-07-24 08:47:39.055] [info] [SimpleCorontine.cpp:229] task result process data done: 201.6 协程await co_await是核心关键字之一用于 “等待一个异步操作完成”并在等待期间暂停当前协程将控制权交还给调用者或调度器。当被等待的操作完成后协程可以从暂停点恢复执行。co_await后跟一个 “可等待对象”awaitable语法形式为
co_await 可等待对象;“可等待对象” 是指实现了特定接口3 个核心方法的对象它代表一个可能尚未完成的异步操作如网络请求、文件 I/O 等。一个对象要能被 co_await 等待必须满足以下条件或通过适配器转换后满足:
await_ready()判断操作是否已完成。 返回 true操作已完成co_await 不暂停直接继续执行。返回 false操作未完成co_await 会暂停协程。 await_suspend(handle)当操作未完成时调用负责注册 “唤醒回调”。 参数 handle 是当前协程的句柄std::coroutine_handle。返回值决定后续行为 返回 void暂停当前协程控制权返回给调用者。返回 false不暂停继续执行当前协程。返回另一个协程句柄切换到该协程执行。 await_resume()当操作完成、协程恢复时调用返回异步操作的结果或抛出异常。 下面是一个简单的通过协程await异步等待的例子
struct AsyncTimer {bool await_ready() {SPDLOG_INFO(await_ready);return false;}void await_suspend(std::coroutine_handle handle) {SPDLOG_INFO(await_suspend);std::thread([this, handle]() {std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));handle.resume(); // 延迟结束恢复协程}).detach();}void await_resume() const noexcept {SPDLOG_INFO(await_resume);}int delay_ms;
};struct Task {struct promise_type {Task get_return_object() {SPDLOG_INFO(get_return_object);return Task{ std::coroutine_handlepromise_type::from_promise(*this) };}std::suspend_never initial_suspend() { SPDLOG_INFO(initial_suspend);return {}; } // 立即执行std::suspend_always final_suspend() noexcept { SPDLOG_INFO(final_suspend);return {}; } // 最终暂停void return_void() {SPDLOG_INFO(return_void);}void unhandled_exception() { std::terminate(); }};std::coroutine_handlepromise_type handle;
};Task async_task() {SPDLOG_INFO(Task start);SPDLOG_INFO(wait 1 seconds........);co_await AsyncTimer{ 1000 }; // 等待1秒异步操作SPDLOG_INFO(wait 1 second done);SPDLOG_INFO(wait another 1 seconds........);co_await AsyncTimer{ 2000 }; // 再等待2秒SPDLOG_INFO(wait 2 second done);
}int testAsyncTask() {auto task async_task();// 等待协程完成简化处理实际需更复杂的调度SPDLOG_INFO(wait task done........);while (!task.handle.done()) {std::this_thread::sleep_for(std::chrono::milliseconds(100));}SPDLOG_INFO(task done);task.handle.destroy(); // 释放资源return 0;
}上面的程序输出为能够看到每次协程等待都会调用对应的await_ready、await_suspend、await_resume方法。并且我们的例子中没有添加co_return那是因为我们的例子不需要返回值如果实际上需要的话还是要加上对应的co_return。
[2025-07-24 08:56:34.288] [info] [SimpleCorontine.cpp:262] get_return_object
[2025-07-24 08:56:34.288] [info] [SimpleCorontine.cpp:266] initial_suspend
[2025-07-24 08:56:34.288] [info] [SimpleCorontine.cpp:283] Task start
[2025-07-24 08:56:34.288] [info] [SimpleCorontine.cpp:284] wait 1 seconds........
[2025-07-24 08:56:34.288] [info] [SimpleCorontine.cpp:239] await_ready
[2025-07-24 08:56:34.288] [info] [SimpleCorontine.cpp:244] await_suspend
[2025-07-24 08:56:34.289] [info] [SimpleCorontine.cpp:296] wait task done........
[2025-07-24 08:56:35.310] [info] [SimpleCorontine.cpp:252] await_resume
[2025-07-24 08:56:35.311] [info] [SimpleCorontine.cpp:286] wait 1 second done
[2025-07-24 08:56:35.311] [info] [SimpleCorontine.cpp:288] wait another 1 seconds........
[2025-07-24 08:56:35.311] [info] [SimpleCorontine.cpp:239] await_ready
[2025-07-24 08:56:35.311] [info] [SimpleCorontine.cpp:244] await_suspend
[2025-07-24 08:56:37.326] [info] [SimpleCorontine.cpp:252] await_resume
[2025-07-24 08:56:37.327] [info] [SimpleCorontine.cpp:290] wait 2 second done
[2025-07-24 08:56:37.327] [info] [SimpleCorontine.cpp:274] return_void
[2025-07-24 08:56:37.327] [info] [SimpleCorontine.cpp:270] final_suspend
[2025-07-24 08:56:37.356] [info] [SimpleCorontine.cpp:301] task done2 深入理解协程 上面谈到了协程的基本原理但是协程的实现原理是比较复杂的上面的例子只是一个简单的例子实际上协程的实现原理是基于状态机的每个协程在不同的状态下会调用不同的方法并且协程的状态是可以切换的。协程的状态机模型使得协程能够在执行过程中挂起和恢复。每个协程都有一个内部状态指示其当前执行位置。状态可以包括
初始状态协程刚被创建尚未开始执行。挂起状态协程执行到 co_await或 co_yield时挂起等待某个事件或值。完成状态协程执行结束所有操作完成。 状态机的状态切换主要通过co_await,co_yield和resume等操作配合控制。而为了更加精细的控制协程C20提供了promise_typepromise_type是协程中用于管理状态和结果的核心组件可以让我们控制协程挂起暂停完成等状态切换时的动作。其中co_await和promise_type相对比较复杂下面就展开描述下。
2.2 协程句柄 协程句柄coroutine handle是一个指向协程的特殊对象允许开发者控制协程的执行状态。它提供了一种机制用于管理和恢复协程的执行。句柄能够用到的关键方法有resume,destroy,promise分别用来恢复协程销毁协程和获取promise对象用来和协程交互。协程句柄大致的接口如下
namespace std::experimental
{templatetypename Promisestruct coroutine_handle;templatestruct coroutine_handlevoid{bool done() const;void resume();void destroy();void* address() const;static coroutine_handle from_address(void* address);};templatetypename Promisestruct coroutine_handle : coroutine_handlevoid{Promise promise() const;static coroutine_handle from_promise(Promise promise);static coroutine_handle from_address(void* address);};}2.2 co_await co_await用于在协程中等待某个操作完成它的作用是暂停协程的执行等待操作完成后再恢复协程的执行。co_await后面的表达式需要是一个Awaitable对象。需要注意的是C20实现中为了提高灵活性、可重用性和性能将co_await接受的对象分为了Awaitable对象和Awaiter。
Awaitable其类型实现了特定的接口使其能与 co_await 关键字一起使用。Awaitable 对象能够在协程中被挂起并在异步操作完成后恢复。如果运算符重载了operator co_await当表达式使用 co_await 时会尝试调用operator co_await。Awaiter实现了await_ready、await_suspend、await_resume三个方法的对象。Awaiter是一个与Awaitable相关的对象负责处理协程的挂起和恢复逻辑。Awaiter提供了方法来管理协程的执行状态。 执行co_await expr表达式时首先将expr转换成一个Awaitable对象然后转换成Awaiter
构建Awaitable对象 如果表达式是由初始挂起点、最终挂起点或 yield表达式产生的Awaitable就是该表达式本身。如果当前协程的 Promise类型具有 await_transform成员函数Awaitable将是 promise.await_transform(expr)的结果。如果不满足以上条件Awaitable就是该表达式本身。 构建Awaiter对象。根据Awaitable对象构造Awaiter对象。 如果 operator co_await的重载解析为单一最佳重载Awaiter就是该调用的结果。 对于成员重载使用 awaitable.operator co_await()。对于非成员重载使用 operator co_await(static_castAwaitable(awaitable))。 如果没有找到合适的重载Awaiter就是 Awaitable本身。 上述过程大致伪代码如下
templatetypename P, typename T
decltype(auto) get_awaitable(P promise, T expr)
{if constexpr (has_any_await_transform_member_vP)return promise.await_transform(static_castT(expr));elsereturn static_castT(expr);
}templatetypename Awaitable
decltype(auto) get_awaiter(Awaitable awaitable)
{if constexpr (has_member_operator_co_await_vAwaitable)return static_castAwaitable(awaitable).operator co_await();else if constexpr (has_non_member_operator_co_await_vAwaitable)return operator co_await(static_castAwaitable(awaitable));elsereturn static_castAwaitable(awaitable);
}获取Awaiter之后就可以根据其定义的await_suspend等实现来对协程进行控制。
{auto value expr;auto awaitable get_awaitable(promise, static_castdecltype(value)(value));auto awaiter get_awaiter(static_castdecltype(awaitable)(awaitable));if (!awaiter.await_ready()){using handle_t std::experimental::coroutine_handleP;using await_suspend_result_t decltype(awaiter.await_suspend(handle_t::from_promise(promise)));suspend-coroutineif constexpr (std::is_void_vawait_suspend_result_t){awaiter.await_suspend(handle_t::from_promise(promise));return-to-caller-or-resumer}else{static_assert(std::is_same_vawait_suspend_result_t, bool,await_suspend() must return void or bool.);if (awaiter.await_suspend(handle_t::from_promise(promise))){return-to-caller-or-resumer}}resume-point}return awaiter.await_resume();
}下面写一个简单的例子来展示Awaitable和Awaiter对象构造过程。
class MyAwaiter {
public:bool await_ready() const noexcept {SPDLOG_INFO(Awaiter: Checking if ready);return false; }void await_suspend(std::coroutine_handle) {SPDLOG_INFO(Awaiter: Coroutine suspended);}int await_resume() {SPDLOG_INFO(Awaiter: Resuming coroutine);return 42; }
};// Awaitable 类
class MyAwaitable {
public:MyAwaitable(std::string v) {SPDLOG_INFO(MyAwaitable::MyAwaitable);}MyAwaiter operator co_await() {SPDLOG_INFO(Awaitable: Co-await called);return MyAwaiter(); // 返回 Awaiter 对象}
};// 协程示例
struct MyCoroutine {struct promise_type {MyCoroutine get_return_object() {SPDLOG_INFO(get_return_object);return MyCoroutine{};}auto initial_suspend() noexcept {SPDLOG_INFO(initial_suspend);return std::suspend_never{};}auto final_suspend() noexcept {SPDLOG_INFO(final_suspend);return std::suspend_never{};}void return_void() {}void unhandled_exception() {}template typename Tauto await_transform(T expr) {SPDLOG_INFO(Awaitable: await_transform called);return MyAwaitable(); // 返回 Awaiter 对象}};
};MyCoroutine start() {SPDLOG_INFO(Coroutine started);co_await ; // 使用 AwaitableSPDLOG_INFO(Coroutine resumed);co_return;
}void testAwaiter(){SPDLOG_INFO(testAwaiter started);auto corn start();SPDLOG_INFO(testAwaiter end);
}上面的代码输出如下和上面描述的流程完全一致。
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:371] testAwaiter started
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:341] get_return_object
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:345] initial_suspend
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:364] Coroutine started
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:357] Awaitable: await_transform called
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:328] MyAwaitable::MyAwaitable
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:332] Awaitable: Co-await called
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:310] Awaiter: Checking if ready
[2025-07-24 22:57:41.604] [info] [SimpleCorontine.cpp:315] Awaiter: Coroutine suspended
[2025-07-24 22:57:41.605] [info] [SimpleCorontine.cpp:373] testAwaiter end根据上面的流程可以看出我们可以根据await的参数来构造Awaitable对象从而获得Awaiter这样就给我们控制协程流程提供了遍历我们可以通过Awaitable对我们的逻辑进行封装可以不同情况使用不同的Awaiter使得逻辑更加清晰更加可扩展。
2.3 promise_type 协程的另一个重点是promise_typepromise_type是一个协程状态控制器用于定义协程的行为包括协程的返回值、异常处理、协程的挂起和恢复等。任何一个协程必须包含promise_type否则无法通过编译。当我们实现了一个协程的promise_type之后其运行的大致流程如下
{co_await promise.initial_suspend();try{body-statements}catch (...){promise.unhandled_exception();}FinalSuspend:co_await promise.final_suspend();
}编译器决定promise_type的类型是根据coroutine_traits来获取的我们可以通过下面方式获取到对应协程的promise_typ需要注意的是协程的参数列表要和模板的列表对应上。
UserAllocCoroutine userAllocCoroutine(MyClass cls, MyClass cls2) {SPDLOG_INFO(Coroutine started.);co_return;SPDLOG_INFO(Coroutine resumed.);
}void testPromiseType(){using promise_type std::coroutine_traitsUserAllocCoroutine, MyClass, MyClass::promise_type;const auto name std::string(typeid(promise_type).name());SPDLOG_INFO(promise type {}, name);
}上面只是大体的流程实际的执行流程有很多细节
使用operator new分配协程帧可选由编译器实现。将所有函数参数复制到协程帧中。调用类型为 P 的promise_type的构造函数。调用 promise.get_return_object()方法获取协程首次暂停时返回给调用者的结果并将其保存为局部变量。调用 promise.initial_suspend()方法并 co_await其结果。当 co_await promise.initial_suspend()表达式恢复执行无论是立即恢复还是异步恢复时协程开始执行你编写的函数体语句。重复6步骤直到执行到co_return 调用 promise.return_void()或 promise.return_value(expr)。按创建顺序的逆序销毁所有自动存储期变量。调用 promise.final_suspend()并 co_await其结果。 当执行过程中发生未被捕获的异常时会触发unhandled_exception
捕获异常并在 catch 块中调用 promise.unhandled_exception()。调用 promise.final_suspend()并 co_await其结果。 从上面的流程能够看出协程的运行基本上都是通过promise_type进行控制的。
2.4 协程帧 函数的执行有栈帧来保存现场恢复现场对应的协程有协程帧在执行时用来保存其局部状态、局部变量、调用栈以及其他上下文信息的结构。和函数栈帧类似协程帧也有其相关的创建和销毁流程相关时机自然不用说分别在协程的调用开头和协程结束点。 协程帧创建和销毁 协程帧通过非数组形式的operator new动态分配内存。如果Promise type定义了类级别的 operator new重载则使用这个重载进行分配否则将使用全局 operator new。但是需要注意的是传递给 operator new的大小不是 sizeof(P)而是整个协程帧的大小。编译器会根据参数数量和大小、promise_type大小、局部变量数量和大小以及管理协程状态所需的其他编译器特定存储自动计算该大小。同时若能确定协程帧的生命周期严格嵌套在调用者的生命周期内且在调用点能确定协程帧所需的大小编译器也会根据优化策略选择省略operator new调用。申请内存有可能会失败针对该情况若promise_type提供了静态成员函数 P::get_return_object_on_allocation_failure()编译器会转而调用 operator new(size_t, nothrow_t)重载。若该调用返回 nullptr协程会立即调用 P::get_return_object_on_allocation_failure()并将结果返回给调用者而非抛出异常。 下面的例子通过重载了operator new/delete操作来hook创建协程帧的动作。
struct UserAllocCoroutine {struct UserAllocPromise {UserAllocPromise(){SPDLOG_INFO(UserAllocPromise constructed.);}// 自定义的 operator newvoid* operator new(std::size_t size) {SPDLOG_INFO(Custom operator new called, size: {} sizeof(UserAllocCoroutine) {}, size, sizeof(UserAllocCoroutine));return ::operator new(size); // 调用全局 operator new}// 自定义的 operator deletevoid operator delete(void* ptr) noexcept {SPDLOG_INFO(Custom operator delete called.);::operator delete(ptr); // 调用全局 operator delete}// 协程返回对象auto get_return_object() {SPDLOG_INFO(get_return_object called.);return UserAllocCoroutine{ std::coroutine_handleUserAllocPromise::from_promise(*this) };}// 处理内存分配失败static auto get_return_object_on_allocation_failure() {SPDLOG_INFO(Allocation failed, returning alternative object.);std::terminate();return UserAllocCoroutine{}; // 返回一个默认构造的协程}// 初始挂起auto initial_suspend() noexcept {SPDLOG_INFO(initial_suspend called.);return std::suspend_always{};}// 最终挂起auto final_suspend() noexcept {SPDLOG_INFO(final_suspend called.);return std::suspend_always{};}void return_void() {SPDLOG_INFO(return_void called.);}void unhandled_exception() {SPDLOG_INFO(unhandled_exception called.);std::exit(1);}};using promise_type UserAllocPromise;std::coroutine_handleUserAllocPromise handle;UserAllocCoroutine(std::coroutine_handleUserAllocPromise h) : handle(h) {SPDLOG_INFO(UserAllocCoroutine constructed.);}UserAllocCoroutine() : handle(nullptr) {SPDLOG_INFO(UserAllocCoroutine default constructed.);}~UserAllocCoroutine() {if (handle) {SPDLOG_INFO(Destroying coroutine.);handle.destroy();}}
};// 协程函数
UserAllocCoroutine userAllocCoroutine() {SPDLOG_INFO(Coroutine started.);co_return;SPDLOG_INFO(Coroutine resumed.);
}int testUserAlloc() {spdlog::set_level(spdlog::level::info); // 设置日志级别auto coroutine userAllocCoroutine(); // 启动协程coroutine.handle.resume(); // 恢复协程return 0;
}对应的输出如下可以看到new/delete分别是在协程开始和结束时调用的。同时能够看到协程帧的大小。
[2025-07-25 20:18:50.618] [info] [SimpleCorontine.cpp:385] Custom operator new called, size: 432 sizeof(UserAllocCoroutine) 8
[2025-07-25 20:18:50.618] [info] [SimpleCorontine.cpp:380] UserAllocPromise constructed.
[2025-07-25 20:18:50.618] [info] [SimpleCorontine.cpp:397] get_return_object called.
[2025-07-25 20:18:50.618] [info] [SimpleCorontine.cpp:434] UserAllocCoroutine constructed.
[2025-07-25 20:18:50.618] [info] [SimpleCorontine.cpp:410] initial_suspend called.
[2025-07-25 20:18:50.618] [info] [SimpleCorontine.cpp:451] Coroutine started.
[2025-07-25 20:18:50.619] [info] [SimpleCorontine.cpp:421] return_void called.
[2025-07-25 20:18:50.619] [info] [SimpleCorontine.cpp:416] final_suspend called.
[2025-07-25 20:18:50.619] [info] [SimpleCorontine.cpp:443] Destroying coroutine.
[2025-07-25 20:18:50.619] [info] [SimpleCorontine.cpp:391] Custom operator delete called.参数复制到协程帧 协程帧的参数复制规则和函数调用的参数复制规则类似如果期望将参数传递给promise_type只需要在promise_type构造函数中添加期望传递的参数即可。同时需要考虑参数的生命周期确保协程访问期间其生命周期是确定的
若参数按值传递则通过调用该类型的移动构造函数将参数复制到协程帧。若参数按引用传递左值引用或右值引用则仅将引用复制到协程帧而非引用指向的值。
struct MyClass{MyClass() {SPDLOG_INFO(MyClass::MyClass);}std::string name ;
};struct UserAllocCoroutine {struct UserAllocPromise {MyClass cls;UserAllocPromise(MyClass cls, MyClass cls2){SPDLOG_INFO(UserAllocPromise constructed.);cls cls;}//省略部分代码};
//省略部分代码
};UserAllocCoroutine userAllocCoroutine(MyClass cls, MyClass cls2) {SPDLOG_INFO(Coroutine started.);co_return;SPDLOG_INFO(Coroutine resumed.);
}2.5 更深入理解协程 之前对于协程的不同操作符等进行了简单的描述为了更加深入理解协程的运作方式本节将通过伪代码来描述不同操作对应的等效代码。假设有以下场景
class task {
public:struct awaiter;class promise_type {public:promise_type() noexcept;~promise_type();struct final_awaiter {bool await_ready() noexcept;std::coroutine_handle await_suspend(std::coroutine_handlepromise_type h) noexcept;void await_resume() noexcept;};task get_return_object() noexcept;std::suspend_always initial_suspend() noexcept;final_awaiter final_suspend() noexcept;void unhandled_exception() noexcept;void return_value(int result) noexcept;private:friend task::awaiter;std::coroutine_handle continuation_;std::variantstd::monostate, int, std::exception_ptr result_;};task(task t) noexcept;~task();task operator(task t) noexcept;struct awaiter {explicit awaiter(std::coroutine_handlepromise_type h) noexcept;bool await_ready() noexcept;std::coroutine_handlepromise_type await_suspend(std::coroutine_handle h) noexcept;int await_resume();private:std::coroutine_handlepromise_type coro_;};awaiter operator co_await() noexcept;private:explicit task(std::coroutine_handlepromise_type h) noexcept;std::coroutine_handlepromise_type coro_;
};task g(int x) {int fx co_await f(x);co_return fx * fx;
}当编译器发现函数包含三个协程关键字co_await、co_yield或co_return中的任何一个时就会开始协程转换过程。其转换的基本步骤如下面描述。
确定promise_type 第一步是通过将签名的返回类型和参数类型作为模板参数代入std::coroutine_traits类型来确定的promise_type。
using __g_promise_t std::coroutine_traitstask, int::promise_type;创建协程state 协程函数需要在暂停时保存协程的状态、参数和局部变量以便在后续恢复时仍可访问。协程状态包含以下几部分
promise_typepromise object所有函数参数的副本关于当前暂停点的信息以及如何恢复 / 销毁协程生命周期跨越暂停点的局部变量 / 临时对象的存储 上面提到过promise_type的构造过程编译器会首先尝试用参数副本的左值引用来调用promise_type构造函数如果有效否则回退到调用promise_type的默认构造函数。这里不再赘述下面是一个简单的辅助函数来描述该过程。
templatetypename Promise, typename... Params
Promise construct_promise([[maybe_unused]] Params... params) {if constexpr (std::constructible_fromPromise, Params...) {return Promise(params...);} else {return Promise();}
}基于此我们添加一个简单的带构造函数的__g_state来描述协程状态。
struct __g_state {__g_state(int x): x(static_castint(x)), __promise(construct_promise__g_promise_t(this-x)){}int x;__g_promise_t __promise;// 待填充
};进入协程之后救护创建协程state用来控制协程如果没有定义operator new则直接走默认的全局new否则使用对应的重载下面就是具体的过程。和之前描述的对齐失败时转到get_return_object_on_allocation_failure处理分配错误。
templatetypename Promise, typename... Args
void* __promise_allocate(std::size_t size, [[maybe_unused]] Args... args) {if constexpr (requires { Promise::operator new(size, args...); }) {return Promise::operator new(size, args...);} else {return Promise::operator new(size);}
}task g(int x) {void* state_mem __promise_allocate__g_promise_t(sizeof(__g_state), x);__g_state* state;try {state ::new (state_mem) __g_state(static_castint(x));if (state nullptr) {return __g_promise_t::get_return_object_on_allocation_failure();}} catch (...) {__g_promise_t::operator delete(state_mem);throw;}// ... 实现启动函数的其余部分
}创建返回对象 创建协程state之后就是调用get_return_object获取返回值这个返回值被存储为局部变量并在启动函数的最后完成其他步骤后返回。我们将上面伪代码中的operator new重载全部替换为全局new来简化逻辑方便查阅。
task g(int x) {std::unique_ptr__g_state state(new __g_state(static_castint(x)));decltype(auto) return_value state-__promise.get_return_object();// ... 实现启动函数的其余部分return return_value;
}初始暂停点 启动函数在调用get_return_object()之后要做的是开始执行协程体而协程体中要执行的第一件事是初始暂停点即求值co_await promise.initial_suspend()。由于从initial_suspend()和可选的operator co_await()返回的对象的生命周期会跨越暂停点它们在协程暂停之前创建在恢复之后销毁这些对象的存储需要放在协程状态中。那考虑如果求值过程中发生了异常那么
以下情况发生的异常会传播回启动函数的调用者并且协程状态会被自动销毁 initial_suspend()的调用对返回的可等待对象的operator co_await()调用如果已定义等待体的await_ready()调用等待体的await_suspend()调用 以下场景发生的异常会被协程体捕获并调用promise.unhandled_exception() await_resume()的调用从operator co_await()返回的对象的析构函数如适用从initial_suspend()返回的对象的析构函数 虽然上面的例子中初始化使用的initial_suspend()返回的是std::suspend_always但是如果返回的其他可等待类型那就有可能发生上面描述的情况。因此需要在协程状态中为它保留存储来控制生命周期这里用suspend_always做示例添加一个manual_lifetime它是可平凡构造和可平凡析构的但允许我们在需要时显式构造 / 析构存储的值。
templatetypename T
struct manual_lifetime {manual_lifetime() noexcept default;~manual_lifetime() default;// 不可复制/移动manual_lifetime(const manual_lifetime) delete;manual_lifetime(manual_lifetime) delete;manual_lifetime operator(const manual_lifetime) delete;manual_lifetime operator(manual_lifetime) delete;templatetypename Factoryrequiresstd::invocableFactory std::same_asstd::invoke_result_tFactory, TT construct_from(Factory factory) noexcept(std::is_nothrow_invocable_vFactory) {return *::new (static_castvoid*(storage)) T(factory());}void destroy() noexcept(std::is_nothrow_destructible_vT) {std::destroy_at(std::launder(reinterpret_castT*(storage)));}T get() noexcept {return *std::launder(reinterpret_castT*(storage));}private:alignas(T) std::byte storage[sizeof(T)];
};基于此在__g_state中添加对应的数据成员。
struct __g_state {__g_state(int x);int x;__g_promise_t __promise;manual_lifetimestd::suspend_always __tmp1;// 待填充
};一旦我们通过调用intial_suspend()构造了这个对象我们就需要调用三个方法来实现co_await表达式await_ready()、await_suspend()和await_resume()。调用await_suspend()时我们需要向它传递当前协程的句柄。目前我们可以只调用std::coroutine_handle__g_promise_t::from_promise()并传递对该promise_type的引用。稍后我们会详细了解其内部工作原理。
task g(int x) {std::unique_ptr__g_state state(new __g_state(static_castint(x)));decltype(auto) return_value state-__promise.get_return_object();state-__tmp1.construct_from([]() - decltype(auto) {return state-__promise.initial_suspend();});if (!state-__tmp1.get().await_ready()) {//// ... 在这里暂停协程//state-__tmp1.get().await_suspend(std::coroutine_handle__g_promise_t::from_promise(state-__promise));state.release();// 向下执行到下面的return语句} else {// 协程没有暂停state.release();//// ... 开始执行协程体//}return return_value;
}记录暂停点 当协程暂停时它需要确保在恢复时能回到暂停时的控制流位置。它还需要跟踪每个暂停点处哪些自动存储期对象处于活动状态以便知道如果协程被销毁而不是恢复时需要销毁什么。实现这一点的一种方法是为协程中的每个暂停点分配一个唯一编号并将其存储在协程状态的整数数据成员中。然后每当协程暂停时它会将暂停点的编号写入协程状态当它被恢复 / 销毁时我们会检查这个整数看看它暂停在哪个暂停点。因此我们扩展协程状态添加一个整数数据成员来存储暂停点索引并将其初始化为 0只需要在适当的时机更新该暂停点的值即可
struct __g_state {__g_state(int x);int x;__g_promise_t __promise;int __suspend_point 0; // -- 添加暂停点索引manual_lifetimestd::suspend_always __tmp1;// 待填充
};实现coroutine_handle::resume()和coroutine_handle::destroy() 调用resume和destroy都会导致协程体的执行只是resume会在暂停点恢复执行而destroy会直接跳转到协程体的结束。在实现 C 协程的coroutine_handle类型时我们需要通过类型擦除的方式存储协程状态的恢复和销毁函数指针以支持对任意协程实例的管理。这种设计使得 coroutine_handle只包含一个指向协程状态的指针并通过状态对象中的函数指针进行恢复和销毁操作同时提供方法在 void*和具体状态之间转换。 此外为了确保函数指针的布局在所有协程状态类型中保持一致我们可以让每个协程状态类型继承自一个包含这些数据成员的基类。这种方法使得协程能够通过任何指向该协程的句柄进行恢复和销毁而不仅限于最近一次调用时传递的句柄。
struct __coroutine_state {using __resume_fn void(__coroutine_state*);using __destroy_fn void(__coroutine_state*);__resume_fn* __resume;__destroy_fn* __destroy;
};在协程handle的resume中只需要调用函数指针即可。
namespace std {templatetypename Promise voidclass coroutine_handle;templateclass coroutine_handlevoid {public:coroutine_handle() noexcept default;coroutine_handle(const coroutine_handle) noexcept default;coroutine_handle operator(const coroutine_handle) noexcept default;void* address() const {return static_castvoid*(state_);}static coroutine_handle from_address(void* ptr) {coroutine_handle h;h.state_ static_cast__coroutine_state*(ptr);return h;}explicit operator bool() noexcept {return state_ ! nullptr;}friend bool operator(coroutine_handle a, coroutine_handle b) noexcept {return a.state_ b.state_;}void resume() const {state_-__resume(state_);}void destroy() const {state_-__destroy(state_);}bool done() const {return state_-__resume nullptr;}private:__coroutine_state* state_ nullptr;};
}实现coroutine_handle::promise()和from_promise() 对于更通用的coroutine_handlePromise特化大多数实现可以直接复用coroutine_handlevoid的实现。然而我们还需要能够访问协程状态的promise_type通过promise()方法返回以及能从promise_type的引用构造coroutine_handle。因此我们需要定义一个新的协程状态基类它继承自__coroutine_state并包含promise_type以便我们可以定义所有使用特定promise_type的协程状态类型都继承自这个基类。同时由于promise_type的构造函数可能需要传递参数副本的引用我们需要promise_type的构造函数在参数副本的构造函数之后调用。因此我们在这个基类中为promise_type预留存储使其相对于协程状态的起始位置有一个固定的偏移量但让派生类负责在参数副本初始化后的适当位置调用构造函数 / 析构函数来实现类似的控制。
templatetypename Promise
struct __coroutine_state_with_promise : __coroutine_state {__coroutine_state_with_promise() noexcept {}~__coroutine_state_with_promise() {}union {Promise __promise;};
};然后更新__g_state类使其继承自这个新基类
struct __g_state : __coroutine_state_with_promise__g_promise_t {__g_state(int __x): x(static_castint(__x)) {// 使用 placement-new 在基类中初始化承诺对象::new ((void*)std::addressof(this-__promise))__g_promise_t(construct_promise__g_promise_t(x));}~__g_state() {// 还需要在参数对象销毁前手动调用承诺析构函数this-__promise.~__g_promise_t();}int __suspend_point 0;int x;manual_lifetimestd::suspend_always __tmp1;// 待填充
};有了上面的基础就可以定义std::coroutine_handlePromise类模板了
namespace std {templatetypename Promiseclass coroutine_handle {using state_t __coroutine_state_with_promisePromise;public:coroutine_handle() noexcept default;coroutine_handle(const coroutine_handle) noexcept default;coroutine_handle operator(const coroutine_handle) noexcept default;operator coroutine_handlevoid() const noexcept {return coroutine_handlevoid::from_address(address());}explicit operator bool() const noexcept {return state_ ! nullptr;}friend bool operator(coroutine_handle a, coroutine_handle b) noexcept {return a.state_ b.state_;}void* address() const {return static_castvoid*(static_cast__coroutine_state*(state_));}static coroutine_handle from_address(void* ptr) {coroutine_handle h;h.state_ static_caststate_t*(static_cast__coroutine_state*(ptr));return h;}Promise promise() const {return state_-__promise;}static coroutine_handle from_promise(Promise promise) {coroutine_handle h;// 我们知道__promise成员的地址因此通过从该地址减去__promise字段的偏移量来计算协程状态的地址h.state_ reinterpret_caststate_t*(reinterpret_castunsigned char*(std::addressof(promise)) -offsetof(state_t, __promise));return h;}// 用coroutine_handlevoid的实现来定义这些void resume() const {static_castcoroutine_handlevoid(*this).resume();}void destroy() const {static_castcoroutine_handlevoid(*this).destroy();}bool done() const {return static_castcoroutine_handlevoid(*this).done();}private:state_t* state_;};
}协程体的开端 先向前声明正确签名的恢复 / 销毁函数并更新__g_state构造函数以初始化协程状态使恢复 / 销毁函数指针指向它们
void __g_resume(__coroutine_state* s);
void __g_destroy(__coroutine_state* s);struct __g_state : __coroutine_state_with_promise__g_promise_t {__g_state(int __x): x(static_castint(__x)) {// 初始化coroutine_handle方法使用的函数指针this-__resume __g_resume;this-__destroy __g_destroy;// 使用placement-new在基类中初始化承诺对象::new ((void*)std::addressof(this-__promise))__g_promise_t(construct_promise__g_promise_t(x));}// ... 其余部分省略以简洁起见
};task g(int x) {std::unique_ptr__g_state state(new __g_state(static_castint(x)));decltype(auto) return_value state-__promise.get_return_object();state-__tmp1.construct_from([]() - decltype(auto) {return state-__promise.initial_suspend();});if (!state-__tmp1.get().await_ready()) {state-__tmp1.get().await_suspend(std::coroutine_handle__g_promise_t::from_promise(state-__promise));state.release();// 向下执行到下面的return语句} else {// 协程没有暂停。立即开始执行体__g_resume(state.release());}return return_value;
}resume/destroy两个函数差不多都是根据暂停点索引生成跳转到代码中正确位置的跳转表区别只是前者需要主动恢复协程后者要销毁对应的数据和状态。
void __g_resume(__coroutine_state* s) {// 我们知道s指向__g_stateauto* state static_cast__g_state*(s);// 根据暂停点索引生成跳转到代码中正确位置的跳转表switch (state-__suspend_point) {case 0: goto suspend_point_0;default: std::unreachable();}suspend_point_0:state-__tmp1.get().await_resume();state-__tmp1.destroy();// TODO: 实现协程体的其余部分//// int fx co_await f(x);// co_return fx * fx;
}void __g_destroy(__coroutine_state* s) {auto* state static_cast__g_state*(s);switch (state-__suspend_point) {case 0: goto suspend_point_0;default: std::unreachable();}suspend_point_0:state-__tmp1.destroy();goto destroy_state;// TODO: 为其他暂停点添加额外逻辑destroy_state:delete state;
}co_await表达式 对于co_await首先需要求值我们的场景中首先需要求值f(x)它返回一个临时的task对象。由于临时task直到语句末尾的分号才会被销毁且该语句包含co_await表达式因此task的生命周期跨越了暂停点因此它必须存储在协程状态中。当对这个临时task求值co_await表达式时我们需要调用operator co_await()方法该方法返回一个临时的awaiter对象。这个对象的生命周期也跨越了暂停点因此也必须存储在协程状态中。
struct __g_state : __coroutine_state_with_promise__g_promise_t {__g_state(int __x);~__g_state();int __suspend_point 0;int x;manual_lifetimestd::suspend_always __tmp1;manual_lifetimetask __tmp2;manual_lifetimetask::awaiter __tmp3;
};既然添加了__tmp2和__tmp3我们需要在__g_destroy函数中添加对应的销毁逻辑。同时注意task::awaiter::await_suspend()方法返回一个协程句柄因此我们需要生成代码来恢复返回的句柄。我们还需要在调用await_suspend()之前更新暂停点索引我们将为此暂停点使用索引 1然后在跳转表中添加一个额外的条目确保我们能回到正确的位置恢复。
void __g_resume(__coroutine_state* s) {// 我们知道s指向__g_stateauto* state static_cast__g_state*(s);// 根据暂停点索引生成跳转到代码中正确位置的跳转表switch (state-__suspend_point) {case 0: goto suspend_point_0;case 1: goto suspend_point_1; // -- 添加新的跳转表条目default: std::unreachable();}suspend_point_0:state-__tmp1.get().await_resume();state-__tmp1.destroy();// int fx co_await f(x);state-__tmp2.construct_from([] {return f(state-x);});state-__tmp3.construct_from([] {return static_casttask(state-__tmp2.get()).operator co_await();});if (!state-__tmp3.get().await_ready()) {// 标记暂停点state-__suspend_point 1;auto h state-__tmp3.get().await_suspend(std::coroutine_handle__g_promise_t::from_promise(state-__promise));// 在返回前恢复返回的协程句柄h.resume();return;}suspend_point_1:int fx state-__tmp3.get().await_resume();state-__tmp3.destroy();state-__tmp2.destroy();// TODO: 实现// co_return fx * fx;
}void __g_destroy(__coroutine_state* s) {auto* state static_cast__g_state*(s);switch (state-__suspend_point) {case 0: goto suspend_point_0;case 1: goto suspend_point_1; // -- 添加新的跳转表条目default: std::unreachable();}suspend_point_0:state-__tmp1.destroy();goto destroy_state;suspend_point_1:state-__tmp3.destroy();state-__tmp2.destroy();goto destroy_state;// TODO: 为其他暂停点添加额外逻辑destroy_state:delete state;
}实现unhandled_exception() 协程的行为就像其函数体被替换为
{promise-type promise promise-constructor-arguments ;try {co_await promise.initial_suspend() ;function-body} catch ( ... ) {if (!initial-await-resume-called)throw ;promise.unhandled_exception() ;}final-suspend :co_await promise.final_suspend() ;
}我们已经在启动函数中单独处理了initial-await_resume-called分支需要处理resume/destroy抛出的异常。如果从返回的协程的.resume()调用中抛出异常它不应被当前协程捕获而应传播出恢复此协程的resume()调用。因此我们将协程句柄存储在函数顶部声明的变量中然后goto到 try/catch之外的点并在那里执行.resume()调用。
void __g_resume(__coroutine_state* s) {auto* state static_cast__g_state*(s);std::coroutine_handlevoid coro_to_resume;try {switch (state-__suspend_point) {case 0: goto suspend_point_0;case 1: goto suspend_point_1; // -- 添加新的跳转表条目default: std::unreachable();}suspend_point_0:state-__tmp1.get().await_resume();state-__tmp1.destroy();// int fx co_await f(x);state-__tmp2.construct_from([] {return f(state-x);});state-__tmp3.construct_from([] {return static_casttask(state-__tmp2.get()).operator co_await();});if (!state-__tmp3.get().await_ready()) {state-__suspend_point 1;coro_to_resume state-__tmp3.get().await_suspend(std::coroutine_handle__g_promise_t::from_promise(state-__promise));goto resume_coro;}suspend_point_1:int fx state-__tmp3.get().await_resume();state-__tmp3.destroy();state-__tmp2.destroy();// TODO: 实现// co_return fx * fx;} catch (...) {state-__promise.unhandled_exception();goto final_suspend;}final_suspend:// TODO: 实现// co_await promise.final_suspend();resume_coro:coro_to_resume.resume();return;
}然而上面的代码存在一个错误。如果__tmp3.get().await_resume()调用抛出异常我们将无法在捕获异常之前调用__tmp3和__tmp2的析构函数。注意我们不能简单地捕获异常、调用析构函数然后重新抛出异常因为这会改变那些析构函数的行为 —— 如果它们调用std::unhandled_exceptions()由于异常已被 “处理”返回值会不同。然而如果析构函数在异常展开期间调用它std::unhandled_exceptions()的调用应该返回非零值。相反我们可以定义一个 RAII 辅助类确保在抛出异常时在作用域退出时调用析构函数。
templatetypename T
struct destructor_guard {explicit destructor_guard(manual_lifetimeT obj) noexcept: ptr_(std::addressof(obj)){}// 不可移动destructor_guard(destructor_guard) delete;destructor_guard operator(destructor_guard) delete;~destructor_guard() noexcept(std::is_nothrow_destructible_vT) {if (ptr_ ! nullptr) {ptr_-destroy();}}void cancel() noexcept { ptr_ nullptr; }private:manual_lifetimeT* ptr_;
};// 对不需要调用析构函数的类型的部分特化
templatetypename Trequires std::is_trivially_destructible_vT
struct destructor_guardT {explicit destructor_guard(manual_lifetimeT) noexcept {}void cancel() noexcept {}
};// 类模板参数推导以简化使用
templatetypename T
destructor_guard(manual_lifetimeT obj) - destructor_guardT;void __g_resume(__coroutine_state* s) {auto* state static_cast__g_state*(s);std::coroutine_handlevoid coro_to_resume;try {switch (state-__suspend_point) {case 0: goto suspend_point_0;case 1: goto suspend_point_1; // -- 添加新的跳转表条目default: std::unreachable();}suspend_point_0:{destructor_guard tmp1_dtor{state-__tmp1};state-__tmp1.get().await_resume();}// int fx co_await f(x);{state-__tmp2.construct_from([] {return f(state-x);});destructor_guard tmp2_dtor{state-__tmp2};state-__tmp3.construct_from([] {return static_casttask(state-__tmp2.get()).operator co_await();});destructor_guard tmp3_dtor{state-__tmp3};if (!state-__tmp3.get().await_ready()) {state-__suspend_point 1;coro_to_resume state-__tmp3.get().await_suspend(std::coroutine_handle__g_promise_t::from_promise(state-__promise));// 协程暂停时不退出作用域// 因此取消析构保护tmp3_dtor.cancel();tmp2_dtor.cancel();goto resume_coro;}// 不要在这里退出作用域//// 我们不能goto到进入具有非平凡析构函数的变量作用域的标签// 因此我们必须在不调用析构函数的情况下退出析构保护的作用域然后在suspend_point_1标签后重新创建它们tmp3_dtor.cancel();tmp2_dtor.cancel();}suspend_point_1:int fx []() - decltype(auto) {destructor_guard tmp2_dtor{state-__tmp2};destructor_guard tmp3_dtor{state-__tmp3};return state-__tmp3.get().await_resume();}();// TODO: 实现// co_return fx * fx;} catch (...) {state-__promise.unhandled_exception();goto final_suspend;}final_suspend:// TODO: 实现// co_await promise.final_suspend();resume_coro:coro_to_resume.resume();return;
}对于promise.unhandled_exception()方法本身抛出异常的情况例如如果它重新抛出当前异常可能需要特殊处理。这种情况下协程需要捕获异常将协程标记为在最终暂停点暂停然后重新抛出异常。
__g_resume(){//省略部分代码............try {// ...} catch (...) {try {state-__promise.unhandled_exception();} catch (...) {state-__suspend_point 2;state-__resume nullptr; // 标记为最终暂停点throw;}}//省略部分代码............
}__g_destroy(){//省略部分代码............switch (state-__suspend_point) {case 0: goto suspend_point_0;case 1: goto suspend_point_1;case 2: goto destroy_state; // 没有需要销毁的作用域内变量// 只需销毁协程状态对象} //省略部分代码............
}实现co_return co_return expr实现相对简单
state-__promise.return_value(fx * fx);
goto final_suspend;实现final_suspend() final_suspend()方法返回一个临时的task::promise_type::final_awaiter类型需要将其存储在协程状态中并在__g_destroy中销毁。这种类型没有自己的operator co_await()因此我们不需要为该调用的结果准备额外的临时对象。与task::awaiter类型一样它也使用返回协程句柄的await_suspend()形式。因此我们需要确保对返回的句柄调用resume()。如果协程不在最终暂停点暂停则协程状态会被隐式销毁。因此如果执行到达协程末尾我们需要删除状态对象。此外由于所有最终暂停逻辑都要求是 noexcept的不需要担心任何子表达式会抛出异常。
struct __g_state : __coroutine_state_with_promise__g_promise_t {__g_state(int __x);~__g_state();int __suspend_point 0;int x;manual_lifetimestd::suspend_always __tmp1;manual_lifetimetask __tmp2;manual_lifetimetask::awaiter __tmp3;manual_lifetimetask::promise_type::final_awaiter __tmp4; // ---
};final_suspend()的实现
final_suspend:// co_await promise.final_suspend{state-__tmp4.construct_from([]() noexcept {return state-__promise.final_suspend();});destructor_guard tmp4_dtor{state-__tmp4};if (!state-__tmp4.get().await_ready()) {state-__suspend_point 2;state-__resume nullptr; // 标记为最终暂停点coro_to_resume state-__tmp4.get().await_suspend(std::coroutine_handle__g_promise_t::from_promise(state-__promise));tmp4_dtor.cancel();goto resume_coro;}state-__tmp4.get().await_resume();}// 如果执行流到达协程末尾则销毁协程状态delete state;return;最终还需要更新__g_destroy函数来处理这个新的暂停点
void __g_destroy(__coroutine_state* s) {auto* state static_cast__g_state*(s);switch (state-__suspend_point) {case 0: goto suspend_point_0;case 1: goto suspend_point_1;case 2: goto suspend_point_2;default: std::unreachable();}suspend_point_0:state-__tmp1.destroy();goto destroy_state;suspend_point_1:state-__tmp3.destroy();state-__tmp2.destroy();goto destroy_state;suspend_point_2:state-__tmp4.destroy();goto destroy_state;destroy_state:delete state;
}实现对称转移和空操作协程 协程规范中强烈建议编译器以尾调用的方式实现下一个协程的恢复而不是递归地恢复下一个协程。这是因为如果协程在循环中相互恢复递归地恢复下一个协程很容易导致无界的栈增长。而上面实现的__g_resume()函数体内调用下一个协程的.resume()然后返回因此__g_resume()帧使用的栈空间要到下一个协程暂停并返回后才会释放。 编译器能够通过将下一个协程的恢复实现为尾调用来做到这一点。通过这种方式编译器生成的代码会先弹出当前栈帧保留返回地址然后执行jmp到下一个协程的恢复函数。由于在 C 中没有机制指定尾位置的函数调用应该是尾调用我们需要从恢复函数返回以便释放其栈空间然后让调用者恢复下一个协程。由于下一个协程在暂停时可能还需要恢复另一个协程而且这可能会无限进行下去调用者需要在循环中恢复协程。这种循环通常称为 “蹦床循环”trampoline loop因为我们从一个协程返回到循环然后从循环 “反弹” 到下一个协程。如果我们将恢复函数的签名修改为返回下一个协程的协程状态指针而不是返回 void那么coroutine_handle::resume()函数可以立即调用下一个协程的__resume()函数指针来恢复它。 因此修改__coroutine_state的__resume_fn签名
struct __coroutine_state {using __resume_fn __coroutine_state* (__coroutine_state*);using __destroy_fn void (__coroutine_state*);__resume_fn* __resume;__destroy_fn* __destroy;
};可以这样编写coroutine_handle::resume()函数:
void std::coroutine_handlevoid::resume() const {__coroutine_state* s state_;do {s s-__resume(s);} while (/* 某种条件 */);
}现在的问题是如何添加终止条件。std::noop_coroutine() 是一个工厂函数返回一个特殊的协程句柄它具有空操作no-op的 resume() 和 destroy() 方法。如果一个协程暂停并从 await_suspend() 方法返回空操作协程句柄这表明没有更多的协程需要恢复恢复此协程的 coroutine_handle::resume() 调用应该返回到其调用者。因此我们需要实现 std::noop_coroutine() 和 coroutine_handle::resume() 中的条件以便当 __coroutine_state 指针指向空操作协程状态时条件返回 false循环退出。我们可以使用的一种策略是定义一个 __coroutine_state 的静态实例指定为空操作协程状态。std::noop_coroutine() 函数可以返回一个指向此对象的协程句柄我们可以将 __coroutine_state 指针与该对象的地址进行比较以查看特定的协程句柄是否是空操作协程。
struct __coroutine_state {using __resume_fn __coroutine_state* (__coroutine_state*);using __destroy_fn void (__coroutine_state*);__resume_fn* __resume;__destroy_fn* __destroy;static __coroutine_state* __noop_resume(__coroutine_state* state) noexcept {return state;}static void __noop_destroy(__coroutine_state*) noexcept {}static const __coroutine_state __noop_coroutine;
};inline const __coroutine_state __coroutine_state::__noop_coroutine{__coroutine_state::__noop_resume,__coroutine_state::__noop_destroy
};namespace std {struct noop_coroutine_promise {};using noop_coroutine_handle coroutine_handlenoop_coroutine_promise;noop_coroutine_handle noop_coroutine() noexcept;templateclass coroutine_handlenoop_coroutine_promise {public:constexpr coroutine_handle(const coroutine_handle) noexcept default;constexpr coroutine_handle operator(const coroutine_handle) noexcept default;constexpr explicit operator bool() noexcept { return true; }constexpr friend bool operator(coroutine_handle, coroutine_handle) noexcept {return true;}operator coroutine_handlevoid() const noexcept {return coroutine_handlevoid::from_address(address());}noop_coroutine_promise promise() const noexcept {static noop_coroutine_promise promise;return promise;}constexpr void resume() const noexcept {}constexpr void destroy() const noexcept {}constexpr bool done() const noexcept { return false; }constexpr void* address() const noexcept {return const_cast__coroutine_state*(__coroutine_state::__noop_coroutine);}private:constexpr coroutine_handle() noexcept default;friend noop_coroutine_handle noop_coroutine() noexcept {return {};}};
}void std::coroutine_handlevoid::resume() const {__coroutine_state* s state_;do {s s-__resume(s);} while (s ! __coroutine_state::__noop_coroutine);
}__coroutine_state* __g_resume(__coroutine_state* s) {auto* state static_cast__g_state*(s);try {switch (state-__suspend_point) {case 0: goto suspend_point_0;case 1: goto suspend_point_1; // -- 添加新的跳转表条目default: std::unreachable();}suspend_point_0:{destructor_guard tmp1_dtor{state-__tmp1};state-__tmp1.get().await_resume();}// int fx co_await f(x);{state-__s1.__tmp2.construct_from([] {return f(state-x);});destructor_guard tmp2_dtor{state-__s1.__tmp2};state-__s1.__tmp3.construct_from([] {return static_casttask(state-__s1.__tmp2.get()).operator co_await();});destructor_guard tmp3_dtor{state-__s1.__tmp3};if (!state-__s1.__tmp3.get().await_ready()) {state-__suspend_point 1;auto h state-__s1.__tmp3.get().await_suspend(std::coroutine_handle__g_promise_t::from_promise(state-__promise));// 协程暂停时不退出作用域// 因此取消析构保护tmp3_dtor.cancel();tmp2_dtor.cancel();return static_cast__coroutine_state*(h.address());}// 不要在这里退出作用域// 我们不能goto到进入具有非平凡析构函数的变量作用域的标签// 因此我们必须在不调用析构函数的情况下退出析构保护的作用域然后在suspend_point_1标签后重新创建它们tmp3_dtor.cancel();tmp2_dtor.cancel();}suspend_point_1:int fx []() - decltype(auto) {destructor_guard tmp2_dtor{state-__s1.__tmp2};destructor_guard tmp3_dtor{state-__s1.__tmp3};return state-__s1.__tmp3.get().await_resume();}();// co_return fx * fx;state-__promise.return_value(fx * fx);goto final_suspend;} catch (...) {state-__promise.unhandled_exception();goto final_suspend;}final_suspend:// co_await promise.final_suspend{state-__tmp4.construct_from([]() noexcept {return state-__promise.final_suspend();});destructor_guard tmp4_dtor{state-__tmp4};if (!state-__tmp4.get().await_ready()) {state-__suspend_point 2;state-__resume nullptr; // 标记为最终暂停点auto h state-__tmp4.get().await_suspend(std::coroutine_handle__g_promise_t::from_promise(state-__promise));tmp4_dtor.cancel();return static_cast__coroutine_state*(h.address());}state-__tmp4.get().await_resume();}// 如果执行流到达协程末尾则销毁协程状态delete state;return static_cast__coroutine_state*(std::noop_coroutine().address());
}协程state的内存占用优化 协程状态类型__g_state实际上比需要的更大。然而一些临时值的生命周期不重叠因此理论上我们可以通过在一个对象的生命周期结束后重用其存储来节省协程状态的空间。由于__tmp2和__tmp3的生命周期重叠我们必须将它们一起放在一个结构体中因为它们都需要同时存在。然而__tmp1和__tmp4的生命周期不重叠因此它们可以一起放在匿名union中。
struct __g_state : __coroutine_state_with_promise__g_promise_t {__g_state(int x);~__g_state();int __suspend_point 0;int x;struct __scope1 {manual_lifetimetask __tmp2;manual_lifetimetask::awaiter __tmp3;};union {manual_lifetimestd::suspend_always __tmp1;__scope1 __s1;manual_lifetimetask::promise_type::final_awaiter __tmp4;};
};3 协程使用可能存在问题
3.1 避免内存分配 异步操作通常需要存储一些每个操作的状态以跟踪操作的进展。这种状态通常需要在操作持续期间保持有效并且只有在操作完成后才能释放。例如调用异步 Win32 I/O 函数时需要分配并传递一个指向 OVERLAPPED 结构的指针。调用者负责确保该指针在操作完成前保持有效。 在传统的基于回调的 API 中这种状态通常需要在堆上分配以确保它具有适当的生命周期。如果您执行多个操作可能需要为每个操作分配和释放这种状态。如果性能是一个问题可以使用自定义分配器从池中分配这些状态对象。然而当我们使用协程时可以避免为操作状态进行堆分配因为协程帧中的局部变量在协程挂起时会保持有效。通过将每个操作的状态放在 Awaiter 对象中我们可以有效地“借用”协程帧的内存用于存储每个操作的状态直到 co_await 表达式完成。一旦操作完成协程恢复Awaiter 对象被销毁从而释放协程帧中的内存供其他局部变量使用。 最终协程帧可能仍然在堆上分配。然而一旦分配协程帧可以用于执行多个异步操作而只需那一次堆分配。如果仔细考虑协程帧实际上充当了一种高性能的区域内存分配器。编译器在编译时确定所需的总区域大小然后能够以零开销的方式将这块内存分配给局部变量。
3.2 理清协程和线程的区别 协程和线程都是用来实现异步编程的手段而已都是在不同维度上所对应的产物。很多文章会将进程线程协程放在一起做描述区分我个人理解其实不需要这么复杂直接从执行层次上区分即可。对于用户态程序来讲其执行代码从上到下的层次分别为协程/函数系统线程逻辑线程或者叫硬件线程这里不做区分。任何用户态的代码最终要运行到CPU上都是要运行到硬件线程单元上的只不过为了方便开发将其线程模型通过操作系统包装成了系统线程一般是m-n模型。系统线程由操作系统调度但是最终都会对应到有限的硬件线程上。协程类似协程的异步是将异步调度权放在了用户态可以认为是在线程上的更上一层包装让用户态可以调度自己的任务。而且按照这个层次以用户态的视角观察越往上切换的开销约小性能越优化开发的灵活性越大。因此C 标准提供的只是最基本的协程支持如果要更合适的调度可以根据自己的开发场景开发对应的协程调度库来方便开发。 当然协程和线程关系又不是那么简单虽然最终协程的代码运行都会落到线程上但是协程的运行规则相比线程要复杂的多需要相比线程更好的调度规划才能达到更好的性能。同时协程可以在一个线程上执行也可以在多个线程上执行这完全取决于开发者的意愿。所以在开发时如果协程的切换存在线程切换也是要考虑多线程问题的。 另外根据现有的开发状态来讲C 协程和线程是不同生态位的东西是相互弥补的。协程的编写和管理相对简单尤其在处理非阻塞 I/O 时可以让代码更清晰避免回调地狱。协程在用户态中进行调度具有更轻量级的特性适合处理大量的异步操作如 I/O 密集型任务。它们能有效减少上下文切换的开销提高程序的响应性。线程能够利用操作系统的调度能力更好地处理需要并行计算的复杂任务。线程则在多核处理器上更有效适合 CPU 密集型任务。线程可以并行执行充分利用多核 CPU 的计算能力。
3.3 对称转移 对称转移是 C20 协程中新增的关键功能允许一个协程暂停时直接将执行权转移给另一个暂停的协程且不产生额外栈空间消耗。其核心是通过await_suspend()返回std::coroutine_handle实现协程间的 “对称” 切换配合编译器的尾调用优化确保栈帧不累积避免传统递归调用导致的栈溢出。同时通过std::noop_coroutine()可在无其他协程可恢复时将执行权返回给resume()的调用者。 在传统协程实现中若协程通过co_await嵌套调用如循环中同步完成的任务会因每次resume()调用在栈上累积帧导致类似递归的栈溢出。考虑下面的例子
// 不支持对称转移的task类型实现会导致栈溢出
class task {
public:class promise_type {public:task get_return_object() noexcept {return task{std::coroutine_handlepromise_type::from_promise(*this)};}std::suspend_always initial_suspend() noexcept { return {}; }void return_void() noexcept {}void unhandled_exception() noexcept { std::terminate(); }struct final_awaiter {bool await_ready() noexcept { return false; }// 直接resume导致栈帧累积void await_suspend(std::coroutine_handlepromise_type h) noexcept {h.promise().continuation.resume(); }void await_resume() noexcept {}};final_awaiter final_suspend() noexcept { return {}; }std::coroutine_handle continuation;};task(task t) noexcept : coro_(std::exchange(t.coro_, {})) {}task(const task) delete;task operator(const task) delete;~task() {if (coro_) coro_.destroy();}class awaiter {public:bool await_ready() noexcept { return false; }// 直接resume被等待协程栈帧叠加void await_suspend(std::coroutine_handle continuation) noexcept {coro_.promise().continuation continuation;coro_.resume(); // 每次调用都会新增栈帧}void await_resume() noexcept {}private:friend task;explicit awaiter(std::coroutine_handlepromise_type h) noexcept : coro_(h) {}std::coroutine_handlepromise_type coro_;};awaiter operator co_await() noexcept {return awaiter{coro_};}// 新增启动协程执行关键修改确保协程实际运行void start() noexcept {if (coro_) coro_.resume();}private:explicit task(std::coroutine_handlepromise_type h) noexcept : coro_(h) {}std::coroutine_handlepromise_type coro_;
};// 同步完成的协程
task completes_synchronously() {co_return; // 立即完成触发final_suspend
}// 循环等待同步任务栈溢出的根源
task loop_synchronously(int count) {for (int i 0; i count; i) {co_await completes_synchronously(); // 每次循环都会嵌套resume}
}// 启动器协程用于触发loop_synchronously执行
task start_loop(int count) {co_await loop_synchronously(count); // 启动循环协程
}int testSym() {spdlog::set_level(spdlog::level::info);spdlog::info(Starting test (will crash due to stack overflow));// 关键修改创建启动器并执行触发完整调用链auto t start_loop(1000000);t.start(); // 启动根协程开始执行整个调用链spdlog::info(This line will never be reached);return 0;
}上述代码中loop_synchronously协程在循环中反复co_await completes_synchronously形成了一种 “隐性递归” 的执行模式。我们通过拆解单次循环的执行步骤分析栈帧的变化
启动根协程初始化栈帧 testSym函数中start_loop(1000000)创建一个task对象随后调用t.start()触发根协程start_loop执行。start_loop的resume()被调用栈上创建第一个栈帧start_loop$resume协程体执行部分。 start_loop等待loop_synchronously栈帧 1 start_loop执行co_await loop_synchronously(1000000)触发loop_synchronously的创建loop_synchronously的协程帧在堆上分配初始挂起后返回task对象。start_loop暂停调用loop_synchronously的await_suspend方法该方法通过coro_.resume()恢复loop_synchronously执行。此时栈上新增第二个栈帧loop_synchronously$resume。 loop_synchronously第一次循环等待completes_synchronously栈帧 2 loop_synchronously进入循环执行co_await completes_synchronously()completes_synchronously创建并挂起返回task对象。 loop_synchronously暂停调用completes_synchronously的await_suspend方法该方法通过coro_.resume()恢复completes_synchronously执行。栈上新增第三个栈帧completes_synchronously$resume。completes_synchronously执行到co_return触发final_suspend其final_awaiter的await_suspend调用loop_synchronously的resume()恢复循环。栈上新增第四个栈帧loop_synchronously$resume第二次进入循环体。 循环累积栈帧无界增长 每次循环迭代中loop_synchronously和completes_synchronously会相互通过resume()恢复对方执行completes_synchronously完成后final_awaiter调用loop_synchronously.resume()栈上新增loop_synchronously$resume帧。loop_synchronously再次co_await时调用completes_synchronously.resume()栈上新增completes_synchronously$resume帧。 每轮循环会新增2 个栈帧且这些帧在循环结束前不会被释放因为resume()的调用者仍在栈上等待返回当循环调用比较多时导致栈帧积累过多导致栈溢出。解决该问题的核心解决方案是避免协程切换时的栈帧累积。以下两个方案
通过await_suspend()返回std::coroutine_handle实现协程间的 “对称转移”配合编译器的尾调用优化在切换协程时释放当前栈帧避免累积。
class task {
public:class promise_type {public:// ...其他代码同前struct final_awaiter {bool await_ready() noexcept { return false; }// 对称转移返回延续协程的句柄尾调用优化std::coroutine_handle await_suspend(std::coroutine_handlepromise_type h) noexcept {return h.promise().continuation; // 直接返回句柄不调用resume()}void await_resume() noexcept {}};// ...其他代码同前};class awaiter {public:// ...其他代码同前// 对称转移返回被等待协程的句柄尾调用优化std::coroutine_handle await_suspend(std::coroutine_handle continuation) noexcept {coro_.promise().continuation continuation;return coro_; // 返回句柄由编译器处理尾调用跳转}};// ...其他代码同前
};使用原子变量检测同步完成。通过std::atomic标记协程是否同步完成若已完成则直接恢复当前协程。
class task {
public:class promise_type {public:// ...其他代码同前std::atomicbool is_completed{false}; // 标记是否已完成std::coroutine_handle continuation;};class awaiter {public:// ...其他代码同前// 返回boolfalse表示直接恢复当前协程bool await_suspend(std::coroutine_handle continuation) noexcept {coro_.promise().continuation continuation;coro_.resume(); // 执行被等待协程// 若已同步完成返回false直接恢复当前协程无栈帧累积return !coro_.promise().is_completed.exchange(true);}};// 修改final_awaiter同步完成时不调用resume()struct final_awaiter {bool await_ready() noexcept { return false; }void await_suspend(std::coroutine_handlepromise_type h) noexcept {h.promise().is_completed true; // 标记完成// 仅在异步完成时才恢复同步完成时由await_suspend直接恢复if (!h.promise().continuation.done()) {h.promise().continuation.resume();}}};
};4 参考文献
Cppreference coroutineCoroutine TheoryC Coroutines: Understanding operator co_awaitC Coroutines: Understanding the promise typeC Coroutines: Understanding Symmetric TransferC Coroutines: Understanding the Compiler Transform