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

C++20 协程

摘要:C++20 引入的协程机制为异步编程提供了轻量级解决方案,其核心优势在于通过用户态调度实现高效的上下文切换,适用于 I/O 密集型任务、生成器模式等场景。本文系统阐述 C++20 协程的底层原理与实践要点,首先解析协程的基本结构,包括promise_type状态控制器、coroutine_handle句柄及co_await、co_yield、co_return关键字的工作机制,揭示协程启动、暂停、恢复与终止的完整生命周期。其次,深入探讨协程的实现细节,如 Awaitable 与 Awaiter 对象的转换逻辑、协程帧的内存管理(分配与释放)、编译器对协程的状态机转换(基于暂停点索引的执行控制)。针对协程使用中的关键问题,分析内存分配优化策略(自定义分配器与内存池)、协程与线程的本质区别(用户态调度 vs 内核态调度),以及对称转移导致的栈溢出风险及解决方案(尾调用优化与std::noop_coroutine)。本文通过实例与伪代码还原协程的编译器转换过程,为开发者理解协程机制、规避常见问题提供理论与实践参考。

关键词:C++20;协程;异步编程;promise_type;上下文切换

1 协程简介

  C++20 引入了协程(Coroutines)支持。协程一种能够暂停和恢复执行的特殊函数,非常适合处理异步操作、生成器、状态机等场景,相比于通过回调函数、线程等方式,协程能够更清晰地组织代码,减少上下文切换的开销,提高代码的可读性和可维护性。

1.1 协程和函数

  在 C++ 中协程是一种特殊的函数调用,它与普通函数存在显著区别。普通函数一旦开始执行,会一直运行到返回语句或函数结束,期间无法暂停;而协程在执行过程中可以在特定点暂停,保存当前的执行状态(包括局部变量、指令指针等),之后在合适的时机可以从暂停点恢复执行,继续完成后续操作。

  从执行流程来看,普通函数的调用是一种栈式的调用方式,遵循 “先进后出” 的原则,每次调用都会在调用栈上分配新的栈帧;协程则拥有自己独立的状态存储,其暂停和恢复不依赖于传统的调用栈,这使得协程的上下文切换成本远低于线程切换。

  简单回顾下普通函数的调用流程,一个普通的函数调用流程如下:

  1. 函数调用(call):程序将控制权转移给被调用函数,同时将返回地址压入调用栈。
  2. 函数执行(execute):被调用函数开始执行,它会使用栈帧来存储局部变量、参数、返回值等。
  3. 函数返回(return):被调用函数执行完毕,将返回值压入栈帧,然后将控制权返回给调用函数。
  4. 调用栈弹出(pop):调用函数从调用栈中弹出返回地址,恢复控制权。

  其中,函数调用时会保存当前的执行状态(包括局部变量、指令指针等),并将控制权交给被调用函数。当被调用函数执行完毕后,会从调用栈中弹出返回地址,恢复调用函数的执行状态,继续执行后续操作。也就是说函数一旦被调用就没有回头路,直到函数调用结束才会将控制权还给调用函数。

  而相比之下协程的调用路程要复杂的多:

  1. 协程启动(start):当协程被调用时,系统会为其分配独立的状态存储(不同于传统栈帧),用于保存局部变量、指令指针、寄存器状态等信息。此时协程进入初始状态,可根据其返回类型的initial_suspend策略决定是否立即执行或暂停。
  2. 协程执行(execute):协程开始执行,与普通函数类似,但在遇到co_awaitco_yield等关键字时会触发暂停逻辑。
    • 若执行co_await expr,协程会先计算表达式expr得到一个 “等待体”(awaitable),然后调用该等待体的awit_suspend方法。若该方法返回true或一个协程句柄,当前协程会暂停,将控制权交还给调用者或切换到其他协程;若返回false,则继续执行。
    • 若执行co_yield expr,本质是通过特殊的等待体暂停协程,并向调用者返回一个值,之后可从暂停点恢复。
  3. 协程暂停(suspend):协程暂停时,其当前执行状态被完整保存到独立存储中,调用栈不会被销毁。此时控制权返回给调用者或调度器,调用者可继续执行其他任务,或在合适时机恢复协程。
  4. 协程恢复(resume):调用者通过协程句柄的resume()方法触发协程恢复。系统从保存的状态中还原执行环境,协程从暂停点继续执行后续代码。
  5. 协程结束(finalize):当协程执行到co_return语句或函数末尾时,会触发final_suspend策略。通常此时协程会进入最终暂停状态,等待调用者通过句柄销毁其状态存储,以释放资源。

  这种流程使得协程能够在执行过程中多次暂停和恢复,且每次切换仅涉及状态存储的读写,无需像线程那样切换内核态上下文,因此效率更高。同时,协程的状态保持特性让异步操作的代码编写更接近同步逻辑,大幅降低了回调嵌套带来的复杂性。

1.2 简单的协程

  下面是一个简单的 C++20 协程示例,展示了协程的基本使用方式:

#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
#include <spdlog/spdlog.h>struct 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,其包括:

  1. 协程函数MySimpleCorontine,用于定义协程的执行逻辑。
  2. 协程Promise类型SimpleCorontinePromise,用于定义协程的状态管理和返回值处理。Promise必须
    • 定义get_return_object()方法,用于返回协程对象。
    • 定义return_void()方法,用于处理协程函数执行完毕后的逻辑。
    • 定义initial_suspend()方法,用于定义协程的初始暂停策略。
    • 定义final_suspend()方法,用于定义协程的最终暂停策略。
    • 定义unhandled_exception()方法,用于处理协程执行过程中发生的异常。
  3. 协程类型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.h>struct SimpleCoroutinePromise;struct SimpleCoroutine {using promise_type = SimpleCoroutinePromise;std::coroutine_handle<promise_type> handle;SimpleCoroutine(std::coroutine_handle<promise_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_handle<SimpleCoroutinePromise>::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来恢复协程的执行,之后再执行协程相关的代码。相关的改动:

  1. 创建一个 handle 来管理协程的生命周期。这个 handle 允许我们控制何时恢复协程的执行以及最终何时销毁它。
  2. 修改suspend状态,其中:
    1. suspend_always:协程在达到指定的挂起点(如initial_suspend())时会暂时停止执行,并将控制权返回给调用者。调用者随后可以选择何时恢复协程的执行。
    2. suspend_never:协程在到达挂起点时直接继续执行,控制权不会返回给调用者。这意味着协程会在达到终点后直接终止,而不会暂停。

1.4 协程yield

  除了基本的结构,协程还有其他的功能,比如yieldco_yield是 C++20 协程中用于 “产出值并暂停” 的关键字,主要用于实现生成器(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_handle<GeneratorPromise>;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_handle<promise_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 -1

1.5 协程co_return

  co_return 是用于终止协程执行并返回结果的关键字,类似于普通函数中的 return,但专门针对协程的特性设计,用于结束协程的生命周期并传递最终结果。co_return 的语法有两种形式:

  • 无返回值:co_return。用于不需要返回最终结果的协程,仅表示协程执行结束。
  • 带返回值:co_return 表达式。用于需要向调用者返回最终结果的协程,表达式的值会被传递给协程的promise_type(promise_type)。

  当协程执行到co_return时,会触发以下流程:

  • 处理返回值:
    • 若为co_return expr;,则表达式 expr的值会被传递给协程promise_typereturn_value(expr)方法(需在 promise_type中定义),由promise_type存储该结果,供调用者获取。
    • 若为 co_return;,则调用promise_typereturn_void()方法(无返回值场景)。
  • 触发最终挂起:
    • 协程执行完返回值处理后,会调用promise_typefinal_suspend()方法,根据其返回的挂起策略(std::suspend_alwaysstd::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_handle<promise_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_handle<promise_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: 20

1.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_handle<promise_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_handle<promise_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_readyawait_suspendawait_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 done

2 深入理解协程

  上面谈到了协程的基本原理,但是协程的实现原理是比较复杂的,上面的例子只是一个简单的例子,实际上协程的实现原理是基于状态机的,每个协程在不同的状态下会调用不同的方法,并且协程的状态是可以切换的。协程的状态机模型使得协程能够在执行过程中挂起和恢复。每个协程都有一个内部状态,指示其当前执行位置。状态可以包括:

  • 初始状态:协程刚被创建,尚未开始执行。
  • 挂起状态:协程执行到 co_awaitco_yield时挂起,等待某个事件或值。
  • 完成状态:协程执行结束,所有操作完成。

  状态机的状态切换主要通过co_await,co_yieldresume等操作配合控制。而为了更加精细的控制协程,C++20提供了promise_typepromise_type是协程中用于管理状态和结果的核心组件,可以让我们控制协程挂起,暂停,完成等状态切换时的动作。其中co_awaitpromise_type相对比较复杂,下面就展开描述下。

2.2 协程句柄

  协程句柄(coroutine handle)是一个指向协程的特殊对象,允许开发者控制协程的执行状态。它提供了一种机制,用于管理和恢复协程的执行。句柄能够用到的关键方法有resume,destroy,promise分别用来恢复协程,销毁协程和获取promise对象用来和协程交互。协程句柄大致的接口如下:

namespace std::experimental
{template<typename Promise>struct coroutine_handle;template<>struct coroutine_handle<void>{bool done() const;void resume();void destroy();void* address() const;static coroutine_handle from_address(void* address);};template<typename Promise>struct coroutine_handle : coroutine_handle<void>{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对象。需要注意的是C++20实现中为了提高灵活性、可重用性和性能,将co_await接受的对象分为了Awaitable对象和Awaiter

  • Awaitable:其类型实现了特定的接口,使其能与 co_await 关键字一起使用。Awaitable 对象能够在协程中被挂起,并在异步操作完成后恢复。如果运算符重载了operator co_await,当表达式使用 co_await 时,会尝试调用operator co_await
  • Awaiter:实现了await_readyawait_suspendawait_resume三个方法的对象。Awaiter是一个与Awaitable相关的对象,负责处理协程的挂起和恢复逻辑。Awaiter提供了方法来管理协程的执行状态。

  执行co_await expr表达式时,首先将expr转换成一个Awaitable对象,然后转换成Awaiter

  1. 构建Awaitable对象
    1. 如果表达式是由初始挂起点、最终挂起点或 yield表达式产生的,Awaitable就是该表达式本身。
    2. 如果当前协程的 Promise类型具有 await_transform成员函数,Awaitable将是 promise.await_transform(expr)的结果。
    3. 如果不满足以上条件,Awaitable就是该表达式本身。
  2. 构建Awaiter对象。根据Awaitable对象构造Awaiter对象。
    1. 如果 operator co_await的重载解析为单一最佳重载,Awaiter就是该调用的结果。
      1. 对于成员重载,使用 awaitable.operator co_await()
      2. 对于非成员重载,使用 operator co_await(static_cast<Awaitable&&>(awaitable))
    2. 如果没有找到合适的重载,Awaiter就是 Awaitable本身。

  上述过程大致伪代码如下:

template<typename P, typename T>
decltype(auto) get_awaitable(P& promise, T&& expr)
{if constexpr (has_any_await_transform_member_v<P>)return promise.await_transform(static_cast<T&&>(expr));elsereturn static_cast<T&&>(expr);
}template<typename Awaitable>
decltype(auto) get_awaiter(Awaitable&& awaitable)
{if constexpr (has_member_operator_co_await_v<Awaitable>)return static_cast<Awaitable&&>(awaitable).operator co_await();else if constexpr (has_non_member_operator_co_await_v<Awaitable&&>)return operator co_await(static_cast<Awaitable&&>(awaitable));elsereturn static_cast<Awaitable&&>(awaitable);
}

  获取Awaiter之后就可以根据其定义的await_suspend等实现来对协程进行控制。

{auto&& value = <expr>;auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));if (!awaiter.await_ready()){using handle_t = std::experimental::coroutine_handle<P>;using await_suspend_result_t =decltype(awaiter.await_suspend(handle_t::from_promise(promise)));<suspend-coroutine>if constexpr (std::is_void_v<await_suspend_result_t>){awaiter.await_suspend(handle_t::from_promise(promise));<return-to-caller-or-resumer>}else{static_assert(std::is_same_v<await_suspend_result_t, bool>,"await_suspend() must return 'void' or 'bool'.");if (awaiter.await_suspend(handle_t::from_promise(promise))){<return-to-caller-or-resumer>}}<resume-point>}return awaiter.await_resume();
}

  下面写一个简单的例子来展示AwaitableAwaiter对象构造过程。

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 T>auto 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_traits<UserAllocCoroutine, MyClass, MyClass>::promise_type;const auto name = std::string(typeid(promise_type).name());SPDLOG_INFO("promise type {}", name);
}

  上面只是大体的流程,实际的执行流程有很多细节:

  1. 使用operator new分配协程帧(可选,由编译器实现)。
  2. 将所有函数参数复制到协程帧中。
  3. 调用类型为 P 的promise_type的构造函数。
  4. 调用 promise.get_return_object()方法获取协程首次暂停时返回给调用者的结果,并将其保存为局部变量。
  5. 调用 promise.initial_suspend()方法并 co_await其结果。
  6. co_await promise.initial_suspend()表达式恢复执行(无论是立即恢复还是异步恢复)时,协程开始执行你编写的函数体语句。
  7. 重复6步骤,直到执行到co_return
    1. 调用 promise.return_void()promise.return_value(<expr>)
    2. 按创建顺序的逆序销毁所有自动存储期变量。
    3. 调用 promise.final_suspend()co_await其结果。

  当执行过程中发生未被捕获的异常时,会触发unhandled_exception

  1. 捕获异常,并在 catch 块中调用 promise.unhandled_exception()
  2. 调用 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_handle<UserAllocPromise>::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_handle<UserAllocPromise> handle;UserAllocCoroutine(std::coroutine_handle<UserAllocPromise> 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_handle<promise_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::variant<std::monostate, int, std::exception_ptr> result_;};task(task&& t) noexcept;~task();task& operator=(task&& t) noexcept;struct awaiter {explicit awaiter(std::coroutine_handle<promise_type> h) noexcept;bool await_ready() noexcept;std::coroutine_handle<promise_type> await_suspend(std::coroutine_handle<> h) noexcept;int await_resume();private:std::coroutine_handle<promise_type> coro_;};awaiter operator co_await() && noexcept;private:explicit task(std::coroutine_handle<promise_type> h) noexcept;std::coroutine_handle<promise_type> coro_;
};task g(int x) {int fx = co_await f(x);co_return fx * fx;
}

    当编译器发现函数包含三个协程关键字(co_awaitco_yieldco_return)中的任何一个时,就会开始协程转换过程。其转换的基本步骤如下面描述。

确定promise_type
  第一步是通过将签名的返回类型和参数类型作为模板参数代入std::coroutine_traits类型来确定的promise_type。

using __g_promise_t = std::coroutine_traits<task, int>::promise_type;

创建协程state
  协程函数需要在暂停时保存协程的状态、参数和局部变量,以便在后续恢复时仍可访问。协程状态包含以下几部分:

  • promise_type(promise object)
  • 所有函数参数的副本
  • 关于当前暂停点的信息以及如何恢复 / 销毁协程
  • 生命周期跨越暂停点的局部变量 / 临时对象的存储

  上面提到过promise_type的构造过程,编译器会首先尝试用参数副本的左值引用来调用promise_type构造函数(如果有效),否则回退到调用promise_type的默认构造函数。这里不再赘述,下面是一个简单的辅助函数来描述该过程。

template<typename Promise, typename... Params>
Promise construct_promise([[maybe_unused]] Params&... params) {if constexpr (std::constructible_from<Promise, Params&...>) {return Promise(params...);} else {return Promise();}
}

  基于此,我们添加一个简单的带构造函数的__g_state来描述协程状态。

struct __g_state {__g_state(int&& x): x(static_cast<int&&>(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处理分配错误。

template<typename 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_cast<int&&>(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_cast<int&&>(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它是可平凡构造和可平凡析构的,但允许我们在需要时显式构造 / 析构存储的值。

template<typename 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;template<typename Factory>requiresstd::invocable<Factory&> &&std::same_as<std::invoke_result_t<Factory&>, T>T& construct_from(Factory factory) noexcept(std::is_nothrow_invocable_v<Factory&>) {return *::new (static_cast<void*>(&storage)) T(factory());}void destroy() noexcept(std::is_nothrow_destructible_v<T>) {std::destroy_at(std::launder(reinterpret_cast<T*>(&storage)));}T& get() & noexcept {return *std::launder(reinterpret_cast<T*>(&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_lifetime<std::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_cast<int&&>(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_lifetime<std::suspend_always> __tmp1;// 待填充
};

实现coroutine_handle::resume()和coroutine_handle::destroy()
  调用resumedestroy都会导致协程体的执行,只是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;
};

  在协程handleresume中只需要调用函数指针即可。

namespace std {template<typename Promise = void>class coroutine_handle;template<>class coroutine_handle<void> {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_cast<void*>(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_handle<Promise>特化,大多数实现可以直接复用coroutine_handle<void>的实现。然而,我们还需要能够访问协程状态的promise_type(通过promise()方法返回),以及能从promise_type的引用构造coroutine_handle。因此,我们需要定义一个新的协程状态基类,它继承自__coroutine_state并包含promise_type,以便我们可以定义所有使用特定promise_type的协程状态类型都继承自这个基类。同时,由于promise_type的构造函数可能需要传递参数副本的引用,我们需要promise_type的构造函数在参数副本的构造函数之后调用。因此我们在这个基类中为promise_type预留存储,使其相对于协程状态的起始位置有一个固定的偏移量,但让派生类负责在参数副本初始化后的适当位置调用构造函数 / 析构函数来实现类似的控制。

template<typename 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_cast<int&&>(__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_lifetime<std::suspend_always> __tmp1;// 待填充
};

  有了上面的基础,就可以定义std::coroutine_handle<Promise>类模板了:

namespace std {template<typename Promise>class coroutine_handle {using state_t = __coroutine_state_with_promise<Promise>;public:coroutine_handle() noexcept = default;coroutine_handle(const coroutine_handle&) noexcept = default;coroutine_handle& operator=(const coroutine_handle&) noexcept = default;operator coroutine_handle<void>() const noexcept {return coroutine_handle<void>::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_cast<void*>(static_cast<__coroutine_state*>(state_));}static coroutine_handle from_address(void* ptr) {coroutine_handle h;h.state_ = static_cast<state_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_cast<state_t*>(reinterpret_cast<unsigned char*>(std::addressof(promise)) -offsetof(state_t, __promise));return h;}// 用coroutine_handle<void>的实现来定义这些void resume() const {static_cast<coroutine_handle<void>>(*this).resume();}void destroy() const {static_cast<coroutine_handle<void>>(*this).destroy();}bool done() const {return static_cast<coroutine_handle<void>>(*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_cast<int&&>(__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_cast<int&&>(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_lifetime<std::suspend_always> __tmp1;manual_lifetime<task> __tmp2;manual_lifetime<task::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_cast<task&&>(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()调用。因此,我们将协程句柄存储在函数顶部声明的变量中,然后gototry/catch之外的点,并在那里执行.resume()调用。

void __g_resume(__coroutine_state* s) {auto* state = static_cast<__g_state*>(s);std::coroutine_handle<void> 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_cast<task&&>(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 辅助类,确保在抛出异常时在作用域退出时调用析构函数。

template<typename T>
struct destructor_guard {explicit destructor_guard(manual_lifetime<T>& obj) noexcept: ptr_(std::addressof(obj)){}// 不可移动destructor_guard(destructor_guard&&) = delete;destructor_guard& operator=(destructor_guard&&) = delete;~destructor_guard() noexcept(std::is_nothrow_destructible_v<T>) {if (ptr_ != nullptr) {ptr_->destroy();}}void cancel() noexcept { ptr_ = nullptr; }private:manual_lifetime<T>* ptr_;
};// 对不需要调用析构函数的类型的部分特化
template<typename T>requires std::is_trivially_destructible_v<T>
struct destructor_guard<T> {explicit destructor_guard(manual_lifetime<T>&) noexcept {}void cancel() noexcept {}
};// 类模板参数推导以简化使用
template<typename T>
destructor_guard(manual_lifetime<T>& obj) -> destructor_guard<T>;void __g_resume(__coroutine_state* s) {auto* state = static_cast<__g_state*>(s);std::coroutine_handle<void> 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_cast<task&&>(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_lifetime<std::suspend_always> __tmp1;manual_lifetime<task> __tmp2;manual_lifetime<task::awaiter> __tmp3;manual_lifetime<task::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_handle<void>::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_handle<noop_coroutine_promise>;noop_coroutine_handle noop_coroutine() noexcept;template<>class coroutine_handle<noop_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_handle<void>() const noexcept {return coroutine_handle<void>::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_handle<void>::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_cast<task&&>(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_lifetime<task> __tmp2;manual_lifetime<task::awaiter> __tmp3;};union {manual_lifetime<std::suspend_always> __tmp1;__scope1 __s1;manual_lifetime<task::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 对称转移

  对称转移是 C++20 协程中新增的关键功能,允许一个协程暂停时直接将执行权转移给另一个暂停的协程,且不产生额外栈空间消耗。其核心是通过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_handle<promise_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_handle<promise_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_handle<promise_type> h) noexcept : coro_(h) {}std::coroutine_handle<promise_type> coro_;};awaiter operator co_await() && noexcept {return awaiter{coro_};}// 新增:启动协程执行(关键修改,确保协程实际运行)void start() noexcept {if (coro_) coro_.resume();}private:explicit task(std::coroutine_handle<promise_type> h) noexcept : coro_(h) {}std::coroutine_handle<promise_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(1'000'000);t.start(); // 启动根协程,开始执行整个调用链spdlog::info("This line will never be reached");return 0;
}

  上述代码中,loop_synchronously协程在循环中反复co_await completes_synchronously,形成了一种 “隐性递归” 的执行模式。我们通过拆解单次循环的执行步骤,分析栈帧的变化:

  1. 启动根协程,初始化栈帧:
    • testSym函数中,start_loop(1'000'000)创建一个task对象,随后调用t.start()触发根协程start_loop执行。
    • start_loopresume()被调用,栈上创建第一个栈帧:start_loop$resume(协程体执行部分)。
  2. start_loop等待loop_synchronously,栈帧 + 1
    • start_loop执行co_await loop_synchronously(1'000'000),触发loop_synchronously的创建:
    • loop_synchronously的协程帧在堆上分配,初始挂起后返回task对象。
    • start_loop暂停,调用loop_synchronouslyawait_suspend方法,该方法通过coro_.resume()恢复loop_synchronously执行。此时栈上新增第二个栈帧:loop_synchronously$resume
  3. loop_synchronously第一次循环,等待completes_synchronously,栈帧 + 2
    • loop_synchronously进入循环,执行co_await completes_synchronously()
    • completes_synchronously创建并挂起,返回task对象。
      loop_synchronously暂停,调用completes_synchronouslyawait_suspend方法,该方法通过coro_.resume()恢复completes_synchronously执行。
    • 栈上新增第三个栈帧:completes_synchronously$resumecompletes_synchronously执行到co_return,触发final_suspend,其final_awaiterawait_suspend调用loop_synchronouslyresume()(恢复循环)。
    • 栈上新增第四个栈帧:loop_synchronously$resume(第二次进入循环体)。
  4. 循环累积,栈帧无界增长
    • 每次循环迭代中,loop_synchronouslycompletes_synchronously会相互通过resume()恢复对方执行:
    • completes_synchronously完成后,final_awaiter调用loop_synchronously.resume(),栈上新增loop_synchronously$resume帧。
    • loop_synchronously再次co_await时,调用completes_synchronously.resume(),栈上新增completes_synchronously$resume帧。

  每轮循环会新增2 个栈帧,且这些帧在循环结束前不会被释放(因为resume()的调用者仍在栈上等待返回),当循环调用比较多时导致栈帧积累过多导致栈溢出。解决该问题的核心解决方案是避免协程切换时的栈帧累积。以下两个方案:

  1. 通过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_handle<promise_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_; // 返回句柄,由编译器处理尾调用跳转}};// ...(其他代码同前)
};
  1. 使用原子变量检测同步完成。通过std::atomic标记协程是否同步完成,若已完成则直接恢复当前协程。
class task {
public:class promise_type {public:// ...(其他代码同前)std::atomic<bool> is_completed{false}; // 标记是否已完成std::coroutine_handle<> continuation;};class awaiter {public:// ...(其他代码同前)// 返回bool:false表示直接恢复当前协程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_handle<promise_type> h) noexcept {h.promise().is_completed = true; // 标记完成// 仅在异步完成时才恢复(同步完成时由await_suspend直接恢复)if (!h.promise().continuation.done()) {h.promise().continuation.resume();}}};
};

4 协程的分类

4.1 有栈协程和无栈协程

  协程根据栈类型主要分为有栈协程(Stackful Coroutines) 和无栈协程(Stackless Coroutines) 两种类型,它们在实现方式、内存占用和适用场景上有显著区别。

  C++20 标准引入的无栈协程的核心特点在于,它们并不拥有独立的调用栈,而是依赖编译器生成的状态机来管理执行流程。这种协程的逻辑在编译时处理,运行时开销极低,因为它不需要为每个协程分配独立的栈空间,内存占用也相对较小(通常仅需保存少量状态变量)。编译器将协程代码转换为状态机,并通过 co_await、co_yield、co_return 等关键字控制暂停和恢复。无栈协程实际上是编译期的状态转换,编译器将协程的逻辑拆分为多个状态块,并通过状态变量追踪执行位置。因此,它的暂停和恢复本质上是状态的切换,而不需处理栈的保存与恢复。这使得切换效率极高,但也限制了它不能在普通函数中随意暂停(必须通过 co_await 等关键字显式标记暂停点)。

  另一方面,有栈协程并不是 C++ 标准原生支持的特性,通常通过第三方库(如 Boost.Coroutine、libco 等)来实现。其核心特点在于拥有独立的调用栈,能够像普通函数一样自由调用其他函数。每个协程都有自己的栈,支持深度函数调用和递归,行为更接近“微型线程”。由于存在独立的栈空间,有栈协程通常需要预先分配栈内存(通常在 KB 级别),因此内存占用较高,创建和切换的开销也相对较大。由于不依赖编译器的特殊支持(依赖库的实现),有栈协程可以在 C++20 之前的版本中使用。有栈协程的运行时栈切换机制允许在任意函数调用中暂停协程(甚至在普通函数中),这使其行为更接近线程的特性,但也因此带来了额外的保存和恢复开销。

4.2 对称协程和非对称协程

  协程根据调度控制权转移方式,可分为对称协程(Symmetric Coroutines) 和非对称协程(Asymmetric Coroutines),这是与 “有栈 / 无栈” 维度正交的分类方式,核心区别在于协程之间如何进行切换。

  非对称协程的核心特点是:协程的暂停和恢复存在明确的 “调用者 - 被调用者” 关系,即协程只能被它的调用者恢复,控制权转移路径是 “单向且层级化” 的。协程 A 创建并启动协程 B 后,B 的暂停只能返回到 A(它的直接调用者),而 B 无法直接切换到另一个无关协程 C。通常通过 resume()(恢复)和 yield()(暂停并返回调用者)两个操作控制,yield() 操作会明确将控制权交回给调用它的协程。由于调度关系明确,状态管理更简单,是大多数编程语言(如 C++20、Python、C#)采用的协程模型。

  对称协程的核心特点是:所有协程地位平等,控制权可以在任意协程之间直接转移,没有固定的 “调用者 - 被调用者” 关系。任何协程可以通过 transfer() 等操作直接将控制权转移给另一个协程,无需经过中间调用者。需要一个全局调度器或协程管理器来协调所有协程的切换,灵活性更高。适合实现复杂的并发模型(如协作式多任务),但实现和调试难度较大。

5 参考文献

  • Cppreference coroutine
  • Coroutine Theory
  • C++ Coroutines: Understanding operator co_await
  • C++ Coroutines: Understanding the promise type
  • C++ Coroutines: Understanding Symmetric Transfer
  • C++ Coroutines: Understanding the Compiler Transform
http://www.lryc.cn/news/600919.html

相关文章:

  • ​机器学习从入门到实践:算法、特征工程与模型评估详解
  • 是德科技 | AI上车后,这条“高速公路”如何畅通?
  • 聚类-一种无监督分类算法
  • 聚类里面的一些相关概念介绍阐述
  • Digit Queries
  • OpenFeign-远程调用
  • 数据结构 二叉树(2)---二叉树的实现
  • excel删除重复项场景
  • HarmonyOS中的PX、 VP、 FP 、LPX、Percentage、Resource 详细区别是什么
  • 商汤InternLM发布最先进的开源多模态推理模型——Intern-S1
  • CUDA杂记--FP16与FP32用途
  • P2392 kkksc03考前临时抱佛脚
  • Linux——线程互斥
  • 【RHCSA 问答题】第 13 章 访问 Linux 文件系统
  • PYTHON从入门到实践-16数据视图化展示
  • 卫星通信终端天线对星之:参考星对星
  • DOM元素添加技巧全解析
  • 单片机CPU内部的定时器——滴答定时器
  • Linux DNS 服务器正反向解析
  • Mybatis学习之配置文件(三)
  • Linux随记(二十一)
  • 变频器实习DAY15
  • Linux内核设计与实现 - 第13章 虚拟文件系统(VFS)
  • Linux shuf命令随机打乱行顺序
  • 差模干扰 共模干扰
  • 利用RAII与析构函数避免C++资源泄漏
  • kafka的部署和jmeter连接kafka
  • 20250726-2-Kubernetes 网络-Service 定义与创建_笔记
  • C++/CLI vs 标准 C++ vs C# 语法对照手册
  • Java 大视界 -- Java 大数据在智能医疗影像数据标注与疾病辅助诊断模型训练中的应用(366)