c++如何实现协程机制_c++ 20 coroutines关键字与异步任务处理【详解】

C++20协程是可扩展底层框架,非开箱即用的async/await;必须正确定义promise_type三要素,co_await按序查找awaiter,跨线程唤醒需通过任务队列投递handle,std::generator不支持异步IO。

标准 C++20 协程不是“实现协程机制”的工具,而是提供了一套可扩展的底层协程框架——它不直接给你 async/await 语义,也不自带调度器或线程池。你必须自己实现 promise_typecoroutine_handle 管理逻辑和挂起点行为,否则编译器只生成一堆无法运行的骨架代码。

为什么 co_await 编译通过却运行崩溃?

最常见原因是未正确定义 promise_type 中的 get_return_object()initial_suspend()unhandled_exception()。C++20 要求这三者必须存在且签名正确,缺一即导致未定义行为(如空指针解引用、栈破坏)。

  • get_return_object() 必须返回一个能持有 coroutine_handle 的对象(如自定义 Task 类),不能只返回 void 或裸 handle
  • initial_suspend() 若返回 suspend_always{},但后续没调用 handle.resume(),协程就永远卡住;若返回 suspend_never{},则需确保 promise 构造完成前不发生异常
  • unhandled_exception() 不实现会导致异常穿透栈帧,直接 std::terminate()

co_await 表达式背后真正调用了哪些函数?

对任意表达式 expr 执行 co_await expr,编译器按顺序尝试查找并调用:

  • expr 自身有 operator co_await() 成员函数,调用它,得到 awaiter 对
  • 否则,若存在 operator co_await(expr) 非成员重载,调用它
  • 否则,直接使用 expr 作为 awaiter(要求它具备 await_ready() / await_suspend() / await_resume() 三个成员)

注意:await_suspend() 返回 void 表示同步恢复;返回 bool 表示是否已自行调度(true = 不恢复,false = 立即恢复);返回 coroutine_handle 则将控制权转交给该 handle。

如何用 std::coroutine_handle 安全跨线程唤醒协程?

协程挂起后,其栈帧通常位于当前线程栈上(除非用了堆分配的 promise),因此 coroutine_handle::resume() 必须在**同一栈上下文**中调用,否则触发未定义行为。安全跨线程唤醒的唯一合规路径是:

  • await_suspend() 中获取目标线程的事件循环句柄(如 io_context、自定义任务队列)
  • coroutine_handle 包装成任务对象,投递到目标线程的任务队列
  • 目标线程从队列取出后,在自身栈上安全调用 handle.resume()

切勿在线程 A 中保存 handle,然后在线程 B 中直接调用 resume()——即使加锁也无效,这是栈生命周期问题,不是竞态问题。

为什么不用 std::generator 直接写异步 HTTP 请求?

std::generator 是为同步迭代器场景设计的:它隐含假设每次 co_yield 后立即被消费者调用 next(),且整个生命周期单线程、无挂起等待。它没有 co_await 支持,不能等待文件描述符就绪、不能交出控制权给 IO 多路复用器。试图强行混用会导致:

  • generator 对象析构时自动调用 destroy(),可能中断正在进行的网络读写
  • 无法在 co_yield 前插入非阻塞等待逻辑(如 epoll_wait
  • 缺乏异常传播通道,网络错误只能靠外部状态轮询,违背协程的“线性错误处理”初衷

真要写异步 HTTP,得从零实现一个带 IO 调度能力的 Task 类型,其中 promise_type 内聚了 epoll/kqueue/IOCP 封装和超时管理——这才是 C++20 协程的真实使用姿势。