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

协程入门(基础篇)

一、协程的基本概念

什么是协程?

协程可以理解为 “可以暂停执行、后续再恢复的函数”。它像一个能 “存档” 和 “读档” 的执行单元,在执行过程中能主动挂起,将 CPU 使用权让给其他任务,之后在合适的时机恢复到挂起时的状态继续执行,整个过程无需操作系统内核参与调度。

比如我们看电影时,按下暂停键(挂起)去接电话,挂完电话再按播放键(恢复),电影会从暂停的地方继续播放,协程的执行逻辑就类似这样。

协程 vs 线程 vs 进程

要理解协程,先对比我们更熟悉的进程和线程,三者的核心差异如下:

特性进程线程协程
调度层级操作系统内核调度操作系统内核调度用户态调度(无内核参与)
上下文切换成本高(切换地址空间、寄存器等)中(共享地址空间,切换寄存器)极低(仅保存函数局部状态)
内存开销大(独立地址空间,通常几 MB 起)中(共享地址空间,栈大小通常几 KB 到几 MB)极小(栈可动态增长,通常几 KB)
并发数量上限少(通常几百到几千)较多(通常几万)极多(通常几十万到几百万)
控制权内核抢占式(强制切换)内核抢占式(强制切换)用户主动式(需手动挂起)

简单来说:进程是 “独立的应用程序”,线程是 “进程内的执行流”,而协程是 “线程内的轻量级执行单元”。

协程、线程、进程的关系总结

  • 进程包含线程: 一个进程可以包含多个线程,线程是进程的组成部分,多个线程共享进程的资源。
  • 线程包含协程: 一个线程可以包含多个协程,协程是在用户态实现的轻量级线程,可以在单个线程内部实现多任务切换。
  • 协程与线程、进程的区别: 协程与线程、进程最大的区别在于调度方式。进程和线程由操作系统调度,协程由用户程序自行调度,切换更加高效。

适用场景对比

  • 进程: 适用于需要高隔离度、稳定性较高的场景,比如独立服务、数据库等。
  • 线程: 适用于计算密集型任务、多任务并发执行、利用多核 CPU 的场景,比如 Web 服务器、数据处理等。
  • 协程: 适用于 IO 密集型任务、异步编程、轻量级多任务切换的场景,比如网络爬虫、异步框架等。

协程的核心特性:挂起与恢复

这是协程区别于普通函数的关键:

  • 挂起:协程在执行到特定位置时(如遇到等待操作),主动暂停执行,保存当前的局部变量、程序计数器等状态,释放 CPU 资源。
  • 恢复:当等待的条件满足(如 IO 操作完成),协程可以从挂起时的状态继续执行,无需重新初始化局部变量。

普通函数一旦执行,要么执行完成返回,要么异常退出,无法 “中途暂停再继续”,而协程弥补了这一缺陷。

二、为什么需要协程?

异步编程的痛点

在传统异步编程中(如多线程、回调函数),我们常会遇到以下问题:

  1. 回调地狱:为了实现顺序执行的异步操作,需要嵌套多层回调函数,代码可读性极差。例如:
// 回调地狱示例:先请求用户数据,再请求订单数据,最后打印结果
requestUserData(userId, [](User user) {requestOrderData(user.id, [](Order order) {printOrder(order); // 嵌套层级深,逻辑混乱});
});
  1. 线程开销过高:高并发场景下,大量线程会导致频繁的内核上下文切换,占用大量 CPU 资源和内存(每个线程栈通常需要几 KB 到几 MB)。
  2. 逻辑割裂:异步操作的代码分散在不同的回调函数中,业务逻辑被拆分,调试和维护难度大。

协程的优势

1. 更高效的并发模型

协程是用户态调度,无需内核参与上下文切换,切换成本仅为普通函数调用的量级(约几纳秒到几十纳秒),远低于线程切换(约几微秒到几十微秒)。在高并发场景(如百万级 IO 请求)下,协程能以极低的资源开销实现高效并发。

2. 更清晰异步代码逻辑

协程可以用 “同步代码的写法” 实现异步操作,避免回调嵌套。例如,用协程重写上面的异步逻辑:

// 协程写法:同步风格实现异步逻辑
void getAndPrintOrder(int userId) {User user = co_await requestUserData(userId); // 挂起等待用户数据Order order = co_await requestOrderData(user.id); // 挂起等待订单数据printOrder(order); // 逻辑连贯,无嵌套
}

代码逻辑和同步编程一致,可读性大幅提升。

3. 更低的内存开销

协程的栈空间可以动态分配和回收,且初始栈大小通常只有几 KB(如 C++ 协程帧通常几 KB),而线程栈默认大小通常为 1MB(Linux 系统)。相同内存下,协程的并发数量可以达到线程的数百倍甚至上千倍。

三、C++20 协程关键字初识

C++20 首次引入协程支持,通过三个关键字实现协程的核心操作,需注意:这些关键字仅能在 “协程函数” 中使用(协程函数的返回类型需满足特定接口)。

1. co_await - 挂起协程

作用:等待一个 “可等待对象”(Awaitable)完成,期间挂起协程,释放 CPU。当 “可等待对象” 完成后,协程从挂起处恢复执行。

场景:常用于等待异步操作(如 IO、定时器)。例如:

// 等待1秒后恢复协程
co_await std::chrono::seconds(1); 
// 等待网络请求完成,获取结果后恢复
auto data = co_await fetchNetworkData(url);

2. co_yield - 产生值并挂起

作用:向协程的调用者返回一个值(类似迭代器的next()),同时挂起协程。下次恢复协程时,从co_yield的下一行继续执行。

场景:常用于生成序列值(如无限序列、分页数据)。例如:

// 生成1~5的序列
generator<int> generateNumbers() {for (int i = 1; i <= 5; ++i) {co_yield i; // 返回i,挂起;下次恢复从下一行继续}
}

调用时可以通过循环获取每个值:

for (auto num : generateNumbers()) {std::cout << num << " "; // 输出:1 2 3 4 5
}

3. co_return - 返回协程结果

作用:结束协程,并返回最终结果(类似普通函数的return)。与普通函数不同,co_return可以返回复杂类型(如自定义对象),且会触发协程的资源清理逻辑。

示例:

// 协程函数:计算两个数的和并返回
task<int> add(int a, int b) {co_return a + b; // 结束协程,返回结果
}

四、第一个协程示例:Hello Coroutine

C++20 协程需要自定义 “协程返回类型”(如taskgenerator),以满足协程的底层接口(包含promise_type)。下面我们实现一个简单的 “Hello Coroutine” 示例:

完整代码

#include <coroutine>
#include <iostream>
#include <string>// 1. 定义协程返回类型:用于无返回值的协程
struct HelloCoroutine {// 协程的核心:promise_type(负责协程的状态管理、结果传递)struct promise_type {// 1. 创建协程返回对象(将promise与返回对象关联)HelloCoroutine get_return_object() {return HelloCoroutine{std::coroutine_handle<promise_type>::from_promise(*this)};}// 2. 协程初始挂起点:return std::suspend_never{} 表示协程创建后立即执行std::suspend_never initial_suspend() { return {}; }// 3. 协程最终挂起点:return std::suspend_never{} 表示协程结束后立即销毁std::suspend_never final_suspend() noexcept { return {}; }// 4. 处理协程的返回(co_return时调用)void return_void() {}// 5. 处理协程异常(可选)void unhandled_exception() { std::terminate(); }};// 协程句柄:用于控制协程的执行(恢复、销毁)std::coroutine_handle<promise_type> handle;// 构造函数:接收协程句柄explicit HelloCoroutine(std::coroutine_handle<promise_type> h) : handle(h) {}// 析构函数:销毁协程句柄,释放协程资源~HelloCoroutine() {if (handle) handle.destroy();}// 禁用拷贝(避免协程句柄重复销毁)HelloCoroutine(const HelloCoroutine&) = delete;HelloCoroutine& operator=(const HelloCoroutine&) = delete;// 移动构造(允许协程对象转移)HelloCoroutine(HelloCoroutine&& other) noexcept : handle(other.handle) {other.handle = nullptr;}HelloCoroutine& operator=(HelloCoroutine&& other) noexcept {if (this != &other) {if (handle) handle.destroy();handle = other.handle;other.handle = nullptr;}return *this;}
};// 2. 定义协程函数:打印"Hello Coroutine!"
HelloCoroutine printHello() {std::cout << "Hello Coroutine!" << std::endl;co_return; // 结束协程(无返回值)
}// 3. 主函数:调用协程
int main() {// 调用协程函数:创建协程并执行(因initial_suspend是suspend_never,立即执行)auto coro = printHello();// 协程已执行完成,main函数结束return 0;
}

代码说明

  1. 协程返回类型HelloCoroutine:必须包含promise_type结构体,这是 C++20 协程的强制要求,promise_type负责协程的状态管理(如创建返回对象、处理挂起 / 恢复、清理资源)。
  2. 协程函数printHello():返回类型为HelloCoroutine,内部用co_return结束协程,执行时会打印 “Hello Coroutine!”。
  3. 主函数调用auto coro = printHello();会创建协程并立即执行(因为initial_suspend返回std::suspend_never),最终打印结果。

运行结果

Hello Coroutine!

五、协程的生命周期

协程从创建到销毁,经历以下 6 个关键阶段,我们结合上面的 “Hello Coroutine” 示例理解:

1. 协程创建

  • 触发时机:调用协程函数(如printHello())时,操作系统不会创建新线程,而是在当前线程中分配 “协程帧”(存储局部变量、程序计数器等状态)。
  • 示例关联printHello()被调用时,首先执行promise_type::get_return_object(),创建HelloCoroutine对象,同时分配协程帧。

2. 协程帧分配

  • 协程帧:类似函数栈帧,但由用户态管理(而非内核),存储内容包括:协程的局部变量(如示例中无显式局部变量)、promise对象、程序计数器(记录当前执行位置)。
  • 内存来源:通常从堆上分配(可自定义分配器优化),销毁时需手动释放(通过coroutine_handle::destroy())。

3. 初始挂起点

  • 触发逻辑:协程创建后,执行promise_type::initial_suspend(),根据返回值决定是否挂起:
    • std::suspend_never:不挂起,立即执行协程函数体(如示例)。
    • std::suspend_always:挂起,需通过coroutine_handle::resume()手动恢复。
  • 示例关联:示例返回std::suspend_never,所以创建后立即执行std::cout << "Hello Coroutine!" << std::endl;

4. 执行流程与挂起 / 恢复

  • 正常执行:若不遇到co_await/co_yield,协程会一直执行到co_return或函数结束。
  • 挂起触发:当执行到co_await(等待异步操作)或co_yield(返回值)时,协程保存当前状态(程序计数器、局部变量),释放 CPU,进入挂起状态。
  • 恢复触发:当挂起条件满足(如异步操作完成、调用者请求下一个值),通过coroutine_handle::resume()恢复协程,从挂起处继续执行。
  • 示例关联:示例无co_await/co_yield,所以直接执行到co_return,无挂起过程。

5. 状态保存与恢复

  • 状态保存:挂起时,协程将程序计数器(下一条要执行的指令)、局部变量、寄存器状态存入协程帧。
  • 状态恢复:恢复时,从协程帧中加载保存的状态,恢复到挂起前的执行上下文,仿佛从未挂起过。

6. 最终挂起点与协程销毁

  • 最终挂起点:协程执行到co_return或异常退出时,会执行promise_type::final_suspend(),根据返回值决定是否挂起:
    • std::suspend_never:不挂起,立即销毁协程帧(如示例)。
    • std::suspend_always:挂起,需手动调用destroy()销毁(用于实现 “协程完成通知” 等场景)。
  • 资源清理:销毁时,首先执行协程帧中局部变量的析构函数,然后释放协程帧内存,最后销毁promise对象。
  • 示例关联:示例返回std::suspend_never,执行co_return后,调用final_suspend(),随后HelloCoroutine析构函数中调用handle.destroy(),释放协程帧。

六、总结

协程的核心价值

  1. 轻量级并发:用户态调度,极低的切换成本和内存开销,适合高并发场景(如百万级 IO 请求)。
  2. 简化异步代码:用同步代码风格实现异步逻辑,避免回调地狱,提升代码可读性和可维护性。
  3. 灵活的状态管理:支持中途挂起与恢复,适合实现生成器、状态机、异步工作流等场景。

适用场景分析

  • 高并发 IO 密集型任务:如网络服务器(处理大量 TCP 连接)、数据库查询、文件读写等(IO 等待时挂起,释放 CPU 处理其他任务)。
  • 生成器场景:如无限序列生成(斐波那契数列)、分页数据加载(每次co_yield返回一页数据)。
  • 状态机场景:如游戏角色状态(待机→移动→攻击,每个状态用协程实现,挂起切换状态)。

学习资源:

(1)管理教程
如果您对管理内容感兴趣,想要了解管理领域的精髓,掌握实战中的高效技巧与策略,不妨访问这个的页面:

技术管理教程

在这里,您将定期收获我们精心准备的深度技术管理文章与独家实战教程,助力您在管理道路上不断前行。

(2)软件工程教程
如果您对软件工程的基本原理以及它们如何支持敏捷实践感兴趣,不妨访问这个的页面:

软件工程教程

这里不仅涵盖了理论知识,如需求分析、设计模式、代码重构等,还包括了实际案例分析,帮助您更好地理解软件工程原则在现实世界中的运用。通过学习这些内容,您不仅可以提升个人技能,还能为团队带来更加高效的工作流程和质量保障。

http://www.dtcms.com/a/482450.html

相关文章:

  • 建设好网站的在线沟通功能广州开发区投资集团有限公司招聘
  • 如何将 iPhone 联系人同步到 Mac
  • 织梦的网站收录不好保定网站建设设计
  • 网络安全之揭秘APT Discord C2 以及如何取证
  • 第五章 神经网络的优化
  • 网络安全主动防御技术与应用
  • 5. 神经网络的学习
  • 响应式网站页面设计怎么写网站建设推广
  • 2025/10/14 redis断联 没有IPv4地址 (自用)
  • 基于多奥品牌设备的车牌识别系统与电梯门禁联动方案,核心是通过硬件信号对接+软件权限映射实现车辆身份与电梯权限的绑定。以下是具体实施步骤:
  • [Backstage] 前端插件 生命周期 | eg构建“云成本”页面
  • extractNativeLibs属性解刨
  • 实现一个通用的 `clone` 函数:从深拷贝到类型安全的 C++ 模板设计
  • dw做网站基础用友财务软件多少钱一年
  • 高端定制网站建设制作网页制作格式
  • java + vue 实现 AI流式输出(打字机效果)
  • Linux网络:使用TCP实现网络通信(服务端)
  • Python Web开发——WSGI接口
  • 第十章:技术路线:成为“技术扫地僧(1)
  • 苹果软件混淆与 iOS 应用加固实录,从被逆向到 IPA 文件防反编译与无源码混淆解决方案
  • Transformers中从 logits 本质到问答系统中的字符定位机制
  • c++11扩展
  • h1z1注册网站百度app官方下载
  • 阮一峰《TypeScript 教程》学习笔记——基本用法
  • LabVIEW腔衰荡信号在线处理系统
  • 为 AI Agent 行为立“规矩”——字节跳动提出 Jeddak AgentArmor 智能体安全框架
  • Arbess CICD实战(12) - 使用Arbess+GitLab实现React.js项目自动化部署
  • 网站如何做延迟加载店铺图片免费生成
  • 【每日算法C#】爬楼梯问题 LeetCode
  • 网站制作很好 乐云践新二级网站建设情况说明书