协程从原理到最新的c++协程特性
1 由IO操作引出协程
类型 | 优点 | 缺点 |
---|---|---|
同步 I/O | 1. 代码逻辑简单,执行流程与逻辑一致,易于理解和编写。 2. 便于调试,可按代码执行顺序排查问题。 | 1. 性能瓶颈突出,I/O 操作时线程阻塞,CPU 空闲造成资源浪费。 2. 并发能力欠佳,高并发场景需创建大量线程,开销大且会导致频繁上下文切换。 |
异步 I/O | 1. 性能出色,I/O 操作不阻塞线程,可继续执行其他任务,充分利用 CPU 资源。 2. 资源利用率高,无需为每个 I/O 请求创建线程,减少系统资源消耗。 | 1. 代码复杂度高,常采用回调函数、事件驱动等方式,易出现回调嵌套,导致“回调地狱”。 2. 调试困难,代码执行顺序与编写顺序不一致,难以跟踪执行流程。 |
在处理 I/O 操作时,是否存在一种既能具备异步操作的高性能,又能保持同步代码简洁逻辑的方式,以方便开发人员进行编程呢?答案是肯定的,可借助轻量级协程达成这一目标。具体而言,在每次执行 send 或 recv 操作前,进行协程切换,同时利用调度器对 epoll_wait 的流程加以优化。
2 协程的实现之工作流程
前言:libco -腾讯开源的框架库
分别讨论三个协程的比较晦涩的工作流程。 第一个协程的创建;第二个 IO 异步操作;第三个协程子过程回调.
2.1创建协程
int co_create(stCoRoutine_t **ppco, const stCoRoutineAttr_t *attr, pfn_co_routine_t pfn, void *arg);
- stCoRoutine_t **ppco:
这是一个指向 stCoRoutine_t 类型指针的指针。stCoRoutine_t 是 libco 库中表示协程的结构体类型。调用 co_create 时,需要传入一个指向空指针的指针,co_create 函数内部会为新的协程对象分配内存,并将指向该协程对象的指针存储在 *ppco 中。 - const stCoRoutineAttr_t *attr:
这是一个指向 stCoRoutineAttr_t 结构体的指针,该结构体用于指定协程的属性,例如栈大小等。如果传入 nullptr,则使用默认的协程属性。在代码中传入 nullptr 就意味着使用 libco 提供的默认属性来创建协程。
合理设置栈大小可以避免栈溢出问题。如果协程执行的任务比较复杂,需要保存较多的局部变量或者有较深的函数调用层次,就需要分配较大的栈空间;反之,如果任务比较简单,可以分配较小的栈空间以节省内存。
- pfn_co_routine_t pfn:
这是一个函数指针,指向协程要执行的函数。pfn_co_routine_t 是 libco 定义的函数类型,该函数接受一个 void* 类型的参数并返回 void* 类型的值。当协程被调度执行时,就会调用这个函数。在代码里,coroutine_func 就是协程要执行的函数。 - void arg:
这是一个 void 类型的参数,用于传递给协程函数 pfn。可以通过这个参数将一些必要的数据传递给协程函数。在代码中传入 nullptr 表示不传递任何额外的数据给协程函数。
2.2 协程的异步操作
hook函数
钩子函数本质上是一种回调函数,它会在某个特定的程序执行点被调用。程序开发者可以预先定义钩子函数的逻辑,当程序运行到特定的事件触发点时,系统会自动调用这个钩子函数,从而实现对程序行为的定制化控制。
以下是一般的实现步骤
- 定义钩子函数:开发者根据需求编写自定义的钩子函数,该函数的参数和返回值类型需要与被替换的原函数保持一致。
- 保存原函数指针:在程序中记录原函数的地址,以便后续可能需要调用原函数的功能。
<- 替换原函数:将原函数的调用地址替换为钩子函数的地址。这样,当程序调用原函数时,实际上会执行钩子函数。- 钩子函数逻辑处理:在钩子函数中,开发者可以添加自定义的逻辑,例如记录日志、修改参数、执行额外的操作等。在处理完自定义逻辑后,可以选择调用原函数完成原本的功能。
你现在只需知道 当你使用read时,会替换操作就行
2.3 回调协程的子过程
在协程创建之后,通常不会马上回调子过程。协程的执行是由调度器控制的,调度器会根据一定的策略来决定何时让协程开始执行。一般来说,当协程被创建并加入到调度器的就绪队列之后,调度器在合适的时机(比如当前正在执行的协程主动让出 CPU 或者等待 I/O 事件等)会从就绪队列中选择一个协程,并开始执行该协程的子过程。
回调子过程主要通过上下文切换来实现。在协程的上下文中设置好回调函数的地址和参数,当进行上下文切换时,CPU 就会跳转到回调函数处开始执行
3 协程的实现之原语操作.
协程的核心原语操作: create, resume, yield exit
-
create
- 功能:用于创建一个新的协程。
- 实现原理:在 libco 中,创建协程时会为协程分配一个独立的栈空间,用于保存协程执行过程中的局部变量和函数调用栈信息。同时,还会初始化协程的上下文环境,包括设置协程的入口函数、参数等。
- 代码示例:假设 co_routine_t 是 libco 中表示协程的结构体,co_create 是创建协程的函数,以下是一个简单的创建协程的代码示例:
co_routine_t* co = NULL;
stCoRoutineAttr_t attr;
attr.stack_size = 1024 * 1024; // 设置栈大小为1MB
co_create(&co, &attr, my_coroutine_func, arg); // my_coroutine_func是协程的入口函数,arg是传递给入口函数的参数
- resume :co_resume(co)
- 用于恢复一个已经暂停的协程的执行
- 功能:用于恢复一个已经暂停的协程的执行。
- 实现原理:当调用 resume 时,libco 会将协程的上下文环境恢复到当前执行线程的寄存器中,使得协程能够从上次暂停的地方继续执行。这涉及到保存当前线程的上下文,然后切换到协程的上下文,让协程在其自己的栈空间上继续执行。
- yiled :co_yield_ct();
- 功能:用于暂停当前协程的执行,将执行权交回给调度器或其他协程。
- 实现原理:yield 操作会保存当前协程的上下文环境,包括寄存器的值、栈指针等信息,以便将来能够恢复执行。然后,它会将执行权转移到其他协程或调度器,使得其他协程有机会执行
- release: co_release(co)
- 功能概述
co_release 函数的主要功能是销毁指定的协程对象,释放该协程所占用的内存和其他相关资源,避免内存泄漏。当一个协程执行完毕或者不再需要时,就应该调用 co_release 函数来清理该协程。
- 功能概述
4 协程的实现之切换
涉及到汇编没有必要了解,只需要了解大概
协程上下文切换是协程实现的核心部分,它使得协程能够暂停执行并在之后恢复执行,从而实现高效的并发编程
上下文切换的基本步骤
- 保存当前协程上下文:当需要切换协程时,首先要保存当前正在执行协程的上下文。这包括将寄存器的值保存到协程对应的上下文结构体中,以及记录当前栈指针的位置。例如,在 x86_64 架构下,可以使用汇编指令将通用寄存器的值依次存储到内存中协程上下文结构体的相应位置。
- 切换栈指针:将栈指针切换到即将执行协程的栈空间。这意味着从当前协程的栈切换到目标协程的栈,以便目标协程能够在其自己的栈上进行操作。
- 恢复目标协程上下文:从目标协程的上下文结构体中读取之前保存的寄存器值,并将其恢复到 CPU 的寄存器中。这样,CPU 就可以从目标协程上次暂停的位置继续执行。
- 继续执行目标协程:完成上述步骤后,CPU 开始执行目标协程的代码。
5. 协程的定义和调度器的定义
struct stCoRoutine_t
{stCoRoutineEnv_t *env;pfn_co_routine_t pfn;void *arg;coctx_t ctx;char cStart;char cEnd;char cIsMain;char cEnableSysHook;char cIsShareStack;void *pvEnv;//char sRunStack[ 1024 * 128 ];stStackMem_t* stack_mem;//save satck buffer while confilct on same stack_buffer;char* stack_sp; unsigned int save_size;char* save_buffer;stCoSpec_t aSpec[1024];};
协程结构体
struct stCoRoutineEnv_t
{stCoRoutine_t *pCallStack[ 128 ];int iCallStackSize;stCoEpoll_t *pEpoll;//for copy stack log lastco and nextcostCoRoutine_t* pending_co;stCoRoutine_t* occupy_co;
};stCoRoutine_t *GetCurrCo( stCoRoutineEnv_t *env )
{return env->pCallStack[ env->iCallStackSize - 1 ];
}
调度器结构体
c++20
c++20 也发展了协程
// 引入输入输出流库,用于标准输入输出操作
#include <iostream>
// 引入 C++20 协程相关的库,支持协程特性
#include <coroutine>
// 引入 <future> 库,提供 std::future 用于异步操作结果的获取
#include <future>#include <vector>
// 引入 <fstream> 库,用于文件输入输出操作
#include <fstream>
#include <string>
// 引入 <filesystem> 库,提供文件系统操作相关功能
#include <filesystem>// 简单的协程返回类型包装器,这是一个模板结构体,T 代表协程返回值的类型
template<typename T>
struct Task {// 协程的 promise_type,它定义了协程的行为和状态管理的关键方法struct promise_type {// 用于存储协程最终返回的值T value_;// 协程启动时的初始挂起策略,std::suspend_never 表示协程启动后立即执行,不挂起std::suspend_never initial_suspend() { return {}; }// 协程结束时的最终挂起策略,std::suspend_never 表示协程结束后不挂起,直接销毁std::suspend_never final_suspend() noexcept { return {}; }// 当协程使用 co_return 语句返回值时,此方法会被调用,将返回值存储到 value_ 中void return_value(T value) { value_ = value; }// 返回一个 Task 对象作为协程的返回对象Task get_return_object() { return {}; }// 当协程中抛出未处理的异常时,此方法会被调用,这里只是空实现,忽略异常void unhandled_exception() {}};// 该方法用于获取协程最终返回的值T get() { return promise_type{}.value_; }
};// 模拟异步文件读取的协程函数,它返回一个 Task 对象,封装了读取文件的结果(字符串类型)
Task<std::string> asyncReadFile(const std::string& filename) {// 输出提示信息,表明当前协程开始读取指定文件std::cout << "Coroutine started reading file: " << filename << std::endl;// 创建一个输入文件流对象,尝试打开指定的文件std::ifstream file(filename);// 用于存储从文件中读取的内容std::string content;// 检查文件是否成功打开if (file.is_open()) {// 用于临时存储每行读取的内容std::string line;// 逐行读取文件内容,直到文件结束while (std::getline(file, line)) {// 将读取到的行添加到 content 中,并添加换行符content += line + '\n';}// 读取完毕后关闭文件file.close();} else {// 如果文件无法打开,输出错误信息到标准错误流std::cerr << "Unable to open file: " << filename << std::endl;}// 输出提示信息,表明当前协程完成了指定文件的读取std::cout << "Coroutine finished reading file: " << filename << std::endl;// 使用 co_return 关键字将读取到的文件内容作为协程的返回值co_return content;
}int main() {// 定义要读取的文件数量const int numFiles = 3;// 定义一个存储 std::future 对象的向量,用于管理异步任务的结果std::vector<std::future<std::string>> futures;// 获取当前工作目录的路径std::filesystem::path currentPath = std::filesystem::current_path();// 创建多个协程来读取不同的文件for (int i = 0; i < numFiles; ++i) {// 构造要读取的文件名,格式为当前路径 + "/test" + 编号 + ".txt"std::string filename = currentPath.string() + "/test" + std::to_string(i) + ".txt";// 调用 asyncReadFile 协程函数,创建一个协程任务auto task = asyncReadFile(filename);// 使用 std::async 模拟并发执行协程,std::launch::async 表示立即启动一个新线程执行任务futures.emplace_back(std::async(std::launch::async, [task]() mutable {// 调用 task 的 get 方法获取协程的返回值return task.get();}));}// 等待所有协程完成并获取结果for (size_t i = 0; i < futures.size(); ++i) {// 调用 std::future 的 get 方法阻塞等待异步任务完成,并获取结果std::string result = futures[i].get();// 输出读取到的文件内容std::cout << "Content of test" << i << ".txt:" << std::endl << result << std::endl;}// 程序正常结束,返回 0return 0;
}
promise_type
协程的返回类型必须包含一个名为 promise_type 的嵌套结构体,这个结构体定义了协程的一些关键操作,像协程启动、结束、返回值处理、异常处理等。promise_type 起到了协程和外部环境之间的桥梁作用,它负责管理协程的状态和返回值.
方法名 | 方法原型 | 作用 | 返回值含义 |
---|---|---|---|
initial_suspend | std::suspend_never initial_suspend() { return {}; } | 决定协程启动时的行为,控制协程启动后是否立即执行 | - std::suspend_never :协程启动后立即执行,不挂起 - std::suspend_always :协程启动后立即挂起,等待外部恢复执行 |
final_suspend | std::suspend_never final_suspend() noexcept { return {}; } | 决定协程结束时的行为,控制协程结束后是否销毁 | - std::suspend_never :协程结束后直接销毁,不挂起 - std::suspend_always :协程结束后挂起,可用于执行清理操作 |
return_value | void return_value(T value) { value_ = value; } | 当协程使用 co_return 语句返回值时被调用,负责存储协程的返回值 | 无,将返回值存储到 promise_type 的成员变量中 |
get_return_object | Task get_return_object() { return {}; } | 返回一个协程的返回对象,外部可通过该对象获取协程的返回值或控制协程执行 | 协程返回类型的实例 |
unhandled_exception | void unhandled_exception() {} | 当协程中抛出未处理的异常时被调用,用于定义异常处理逻辑 | 无,可在方法内实现异常处理,如记录日志、重新抛出异常等 |
co_return
co_return 的实现依赖于协程的返回类型的 promise_type。当协程执行到 co_return 时,编译器会调用 promise_type 中的 return_value 方法,将返回值存储到 promise_type 中定义的成员变量里。