C++20中的协程(Coroutines)解决了什么问题?(简化异步编程与挂起操作)

C++20协程通过co_await等关键字将异步逻辑显式建模为可挂起的表达式求值,编译器自动生成状态机管理局部变量和控制流,但需注意内存分配、异常安全、调度语义及调试限制。

协程让异步代码写得像同步代码一样自然

传统回调或 std::future 写异步逻辑时,控制流被拆得支离破碎:状态要手动保存、错误要层层传递、资源生命周期难管理。C++20 协程通过 co_awaitco_yieldco_return 把挂起/恢复点显式标记出来,编译器自动生成状态机,把堆栈局部变量“冻结”进协程帧(coroutine frame)里——你不用手写状态枚举、switch 分支、上下文指针传递。

co_await 的核心不是“等待”,而是“可挂起的表达式求值”

co_await 不是语法糖,它要求操作数提供 await_ready()await_suspend()await_resume() 三个成员函数。这意味着你能控制:什么时候真正挂起、挂起后把控制权交给谁(比如调度器)、恢复时返回什么值。

  • 常见误区:以为 co_await some_task 等同于 “等它完成”,其实它可能立刻返回(await_ready() == true),也可能挂起并调用 await_suspend() 去注册回调
  • 典型实现中,await_suspend() 返回 void 表示立即挂起;返回 bool 可决定是否继续执行(true 表示已安排好唤醒,当前协程可安全暂停;false 表示不挂起,继续跑)
  • 不要在 await_resume() 里抛异常——它运行在恢复上下文中,异常传播路径和普通函数不同,容易导致未定义行为

协程帧内存分配不可忽略,尤其在嵌入式或高频场景

默认情况下,协程帧由 operator new 在堆上分配,且无法被编译器优化掉。这对性能敏感场景(如网络包处理、实时音频)是隐患。

  • 可用 promise_type::get_return_object_on_allocation_failure() 拦截分配失败,但 C++20 标准没强制要求实现该机制,实际依赖编译器支持(如 MSVC 支持,GCC 12+ 部分支持)
  • 更可靠的方式是重载 operator newoperator delete 在 promise type 中,绑定到对象池或栈内存(需确保生命周期可控)
  • Clang/GCC 编译时加 -fsanitize=coroutine 可检测协程帧生命周期误用,比如挂起后 promise 对象已被析构

协程不是万能的,别用它替代线程或简单循环

协程本质是用户态协作式调度,不解决 CPU 密集型任务的并行问题。它适合 I/O 等待、事件驱动、生成器这类“逻辑上需中断、物理上不占 CPU”的场景。

  • 若在 co_await 后面直接跟一个忙等待循环(如 while(!ready) {}),协程不会挂起,反而阻塞整个线程——这违背了协程设计初衷
  • 多个协程共享同一线程时,一个协程长时间不 co_await,会饿死其他协程;没有内置抢占机制,必须靠程序员主动让出控制权
  • 调试困难:GDB/LLDB 对协程帧的支持仍有限,bt 可能只显示 __builtin_coro_resume,看不到原始调用链
struct task {
  struct promise_type {
    task get_return_object() { return {}; }
    std::suspend_never initial_sus

pend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() { std::terminate(); } void return_void() {} }; }; task example() { std::cout << "before await\n"; co_await std::suspend_always{}; // 挂起 std::cout << "after await\n"; // 恢复后才执行 }
协程的价值不在语法炫技,而在于把“等待外部事件”这件事从控制流中解耦出来。但它的复杂性也藏在细节里:内存模型、异常安全、调试可见性、调度策略——这些地方稍不注意,就比手写状态机还难排查。