• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2026-08-05 12:34 Aet 隐藏边栏 |   抢沙发  2 
文章评分 1 次,平均分 5.0

概述

  1. C++20 协程的核心不是“创建线程”,而是
    1. 把一个函数编译成可以暂停、保存现场、稍后恢复的状态机
  2. 协程本身不提供线程、调度器、事件循环或异步 I/O
    1. 它只提供语言机制,真正决定“何时恢复”的是 Awaiter、线程池、Asio 等运行时

关键字

co_await

  1. 等待某个操作,必要时暂停协程

  1. 上面的操作大致经过三个阶段

co_yield

  1. 产生一个值,然后暂停协程
  2. co_yield 适合逐个产生数据
    1. C++20 标准库没有 std::generator
    2. C++23 才引入它。C++20 需要库提供或自己实现

  1. 作用
  2. 技巧

co_return

  1. 结束协程,可以返回结果
    1. 协程不能用普通 return 返回结果

其他

await_ready

  1. 返回 true

  1. 返回 false

await_suspend

  1. 协程已经保存现场,Awaiter 获得协程句柄
  2. 它通常会:
    1. 把句柄加入线程池
    2. 注册到 epoll/IOCP
    3. 交给定时器
    4. 保存到异步操作对象
    5. 在操作完成时调用 handle.resume()

await_resume

  1. 它负责
    1. 返回异步结果
    2. 检查错误
    3. 必要时抛出异常

promise_type 职责

  1. promise_type 不是 std::promise。只是名字相似
    1. std::promise 属于 future 体系
    2. 协程的 promise_type 是编译器与协程返回类型之间的协议
  2. 编译器通过返回类型找到

  1. promise_type 可以定义这些接口

实现原理:协程帧

概述

  1. 普通函数的局部变量通常放在调用线程栈上
  2. 协程可能在函数返回后继续存在,所以跨越暂停点的状态不能只放在普通调用栈中
  3. 编译器会创建协程帧:

  1. 协程帧通常分配在堆上,但标准允许编译器消除分配或嵌入调用者存储中
    1. 因此不能简单宣称“协程一定会进行堆分配”

编译器大致如何改写

  1. 原始协程

  1. 概念上会被改写为状态机

  1. 核心本质是

生命周期问题

引用参数悬空

  1. 如果协程暂停期间 text 被销毁,恢复后就会访问悬空引用
    1. 协程帧保存的是引用,不会自动复制引用所指向的对象

  1. 必要时按值传递

协程 lambda 捕获悬空

  1. 如果 lambda 闭包在协程恢复前销毁,协程可能通过悬空的 this 访问捕获成员

  1. 更稳妥的方式是把数据作为协程函数参数传入,并确保进入协程帧

协程帧必须准确销毁一次

  1. 会销毁
    1. promise
    2. 协程帧中的局部变量
    3. 参数副本
    4. 协程帧存储

  1. 不能
    1. 重复 destroy()
    2. 销毁后再 resume()
    3. 对已经到达最终暂停点的协程再次 resume()
    4. 协程仍可能被异步回调恢复时提前销毁它
  2. std::coroutine_handle 本身不会自动管理生命周期,通常需要 TaskGeneratorRAII 包装它

不要并发恢复同一个协程

  1. 多个线程同时执行
    1. 通常会破坏协程帧状态,产生数据竞争或未定义行为
    2. 调度器必须保证一个协程同一时刻只被一个执行流恢复

什么时候使用协程

适合

  1. 高并发网络连接
  2. 异步文件和数据库操作
  3. 定时器
  4. GUI 事件序列
  5. Generator
  6. 状态机
  7. 多个异步操作的顺序组合
  8. 减少回调嵌套

不一定适合

  1. 简单同步计算
  2. CPU 密集任务但没有线程池
  3. 极短且数量巨大的任务,协程帧成本不可忽略
  4. 库没有提供 Awaiter/调度器
  5. 生命周期很难证明
  6. 团队尚未建立统一的 Task、取消和错误协议

三个核心结论

  1. C++20 协程是编译器状态机转换机制,不是线程或调度器
  2. co_await 是否暂停、在哪里恢复,完全由 Awaiter 协议决定
  3. 真正困难的不是三个关键字,而是协程帧所有权、异步操作生命周期、取消、异常和调度器

最小可运行协程

code

  1. C++20 标准库没有直接提供通用的 Task 类型,所以需要自己定义返回对象

执行过程

  1. 调用
    1. 并不是像普通函数一样从头执行到尾

  1. 执行过程是

suspend_always

  1. 表示一定暂停

suspend_never

  1. 表示不暂停

协程在网络编程中

Asio 回调

Asio 的协程接口

  1. 可以写成更接近同步流程的代码

  1. 协程版本看起来像阻塞代码,但底层仍然是异步模型

高级技巧

协程组合:Task 等待 Task

  1. 希望写出
    1. 外层协程等待内层协程

  1. 典型实现方式是
    1. 外层协程执行 co_await inner_task
    2. 将外层句柄保存为内层的 continuation;
    3. 启动或恢复内层协程
    4. 内层进入 final_suspend()
    5. 内层恢复外层 continuation
    6. 外层通过 await_resume() 获取结果

对称转移

  1. await_suspend() 不一定返回 void,还可以返回另一个协程句柄

  1. 运行时可以直接从当前协程转移到另一个协程
    1. resume() 的递归调用
    2. 深层协程组合造成的调用栈增长
    3. 不必要的调度往返

异常传递

  1. 协程体中未捕获的异常不会直接随意逃出,而是调用

  1. await_resume() 中重新抛出

取消

  1. C++20 协程本身没有自动取消功能

  1. 但仅检查 stop_token 不一定能取消已经注册的异步 I/O。真正取消通常还需要:
    1. epoll 等待表移除
    2. 调用 CancelIoEx
    3. 取消定时器
    4. 从线程池队列撤回任务
    5. 决定协程如何恢复并返回取消错误

调度线程切换

  1. 协程从哪个线程恢复,取决于谁调用

  1. 协程不具备线程亲和性。如果后续代码必须回到 UI 线程或 io_context 线程,需要显式切换调度器

避免锁跨越暂停点

  1. 危险写法

  1. 协程暂停时,局部变量仍然存活,因此 mutex 会一直保持锁定,直到协程恢复并离开作用域
  2. 这可能造成:
    1. 长时间持锁
    2. 死锁
    3. 其他线程完全无法推进
    4. 异步操作等待的逻辑反过来需要这把锁
  3. 应当在暂停前释放锁:

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享