1. 项目概述为什么我们需要C协程如果你写过C的网络服务或者任何涉及I/O密集型操作的代码大概率对“回调地狱”这个词深有体会。异步编程这个旨在提升程序并发能力和资源利用率的利器在传统的基于回调或Future/Promise的模型下代码结构往往会变得支离破碎逻辑跳转让人眼花缭乱。一个简单的“连接-发送-接收”流程可能就需要嵌套五六层回调函数调试起来如同在迷宫里找出口。这正是“C协程实战异步编程代码实现”这个项目要解决的核心痛点。C20标准正式引入的协程Coroutines特性为我们提供了一种全新的、更符合人类线性思维的方式来编写异步代码。它允许你将一个可能被挂起和恢复的函数写成顺序执行的样子底层由编译器和运行时库来处理复杂的状态保存与恢复。简单来说就是用写同步代码的直观性获得异步代码的高性能。这不仅仅是语法糖而是一种编程范式的转变。它尤其适合网络编程、文件I/O、定时任务调度等场景。通过本项目我将带你从零开始手把手实现一个基于C20协程的简易异步任务框架并深入剖析其背后的原理。无论你是想优化现有项目的异步架构还是单纯对这门“黑魔法”感到好奇这篇实战指南都将为你提供可直接复现的代码和踩坑经验。2. 协程核心概念与C20实现机制拆解在动手写代码之前我们必须先理解C20协程的几个核心“零件”。很多教程一上来就抛出一堆术语让人望而却步。我这里用一个更生活化的比喻来解释你可以把协程想象成一个可以随时暂停和继续播放的视频播放器。2.1 核心组件角色扮演协程函数 (Coroutine Function) 就是那个“视频内容”。任何包含co_await,co_yield,co_return关键字的函数都会被编译器特殊处理变成一个协程。它定义了主要的执行逻辑。承诺类型 (Promise Type) 这是协程的“制片人”和“导演”。它负责协程的“生命周期管理”和“产出物管理”。具体来说它要定义协程的返回值对象get_return_object。初始挂起点initial_suspend协程一开场是直接开拍还是先暂停最终挂起点final_suspend协程拍完了co_return或函数体结束后是自动清理片场销毁协程帧还是暂停等待别人来清理如何处理co_yield产生的值yield_value。如何处理未捕获的异常unhandled_exception。协程句柄 (coroutine_handle) 这是“播放器的遥控器”。通过它外部代码可以恢复resume()或销毁destroy()一个被挂起的协程。它也是访问协程内部状态比如promise对象的入口。等待体与等待器 (Awaitable Awaiter) 这是决定“何时暂停播放”的规则。co_await expr中的expr必须是一个可等待体 (Awaitable)。编译器会尝试从中获取一个等待器 (Awaiter)。等待器有三个关键方法await_ready() 问“视频下一段缓冲好了吗”。如果返回true就不暂停直接继续播。await_suspend(coroutine_handle) 如果没缓冲好await_ready()返回false就在这里注册回调“等缓冲好了就用这个遥控器句柄恢复播放”。然后当前协程暂停。await_resume() 当协程被恢复后这个方法的返回值就是co_await表达式的结果。2.2 编译器背后的魔法协程帧当你调用一个协程函数时编译器并不会像普通函数那样直接在调用栈上分配空间。它会在堆上或通过定制分配器分配一块称为协程帧 (Coroutine Frame)的内存。这块内存里存放了传递给协程的参数。协程函数体内的局部变量包括编译器生成的隐藏状态变量。promise对象。当前挂起点的位置一个恢复地址。其他必要的簿记信息。这正是“无栈协程”名称的由来——它的状态保存在堆上独立的帧里而不是传统的函数调用栈上。挂起时保存当前执行点恢复时从保存的点继续执行并恢复所有局部变量的值。这个转换过程由编译器自动完成通常是通过将函数体转换成一个包含巨大switch语句的状态机来实现的。注意 协程帧的分配可能带来动态内存开销这是性能考量的一个点。高性能库通常会提供自定义分配器如内存池来优化这一点。2.3co_await的工作流程理解co_await的流程是理解协程异步操作的关键。当执行到co_await expr;时获取等待器 通过expr获取awaiter对象可能经过operator co_await转换。询问就绪 调用awaiter.await_ready()。若为true跳至步骤5。若为false继续步骤3。挂起协程 挂起当前协程保存所有局部变量和当前执行位置到协程帧。安排恢复 调用awaiter.await_suspend(handle)。这是异步操作的核心在这个方法里你通常会将恢复协程的句柄handle注册到某个异步操作的回调中例如将handle交给epoll或IOCP当数据可读时调用handle.resume()。然后函数返回控制权交还给调用者或调度器。恢复与取值 当异步操作完成协程被恢复通过handle.resume()后执行awaiter.await_resume()其返回值作为co_await表达式的结果然后继续执行后续代码。3. 实战从零构建一个简易协程任务框架理论说得再多不如一行代码。我们现在就来构建一个最简单的协程任务框架SimpleTask。这个框架不依赖boost::asio等大型库旨在揭示最纯粹的C20协程机制。我们将实现一个可以co_await的“睡眠”定时器任务。3.1 定义承诺类型和任务返回值首先我们需要定义协程的返回类型SimpleTask和其内部的承诺类型PromiseType。#include coroutine #include iostream #include chrono #include thread #include functional #include queue #include atomic #include memory // 前向声明 struct SimpleTask; // 承诺类型 struct PromiseType { // 1. 获取返回值对象一个包装了本承诺的 SimpleTask SimpleTask get_return_object() noexcept; // 2. 初始挂起策略协程开始即暂停让调用者决定何时启动 std::suspend_always initial_suspend() noexcept { return {}; } // 3. 最终挂起策略协程结束后也暂停我们需要手动销毁协程帧 std::suspend_always final_suspend() noexcept { return {}; } // 4. 没有返回值本例中任务不产生值只表示完成 void return_void() noexcept {} // 5. 异常处理简单打印并重新抛出实际项目需更健壮 void unhandled_exception() { std::rethrow_exception(std::current_exception()); } };std::suspend_always和std::suspend_never是标准库提供的两个最简单的Awaiter它们只实现await_ready分别返回false和true和await_suspend空操作。3.2 定义任务类型SimpleTaskSimpleTask是协程对外的接口它持有一个协程句柄。struct SimpleTask { // 别名让编译器知道我们的承诺类型 using promise_type PromiseType; // 构造函数接收一个协程句柄 explicit SimpleTask(std::coroutine_handlePromiseType handle) noexcept : coro_handle(handle) {} // 析构时如果协程句柄有效且协程未结束需要销毁它防止内存泄漏 ~SimpleTask() { if (coro_handle coro_handle.done()) { coro_handle.destroy(); } } // 移动构造/赋值管理句柄所有权 SimpleTask(SimpleTask other) noexcept : coro_handle(std::exchange(other.coro_handle, nullptr)) {} SimpleTask operator(SimpleTask other) noexcept { if (this ! other) { if (coro_handle coro_handle.done()) { coro_handle.destroy(); } coro_handle std::exchange(other.coro_handle, nullptr); } return *this; } // 删除拷贝句柄资源唯一 SimpleTask(const SimpleTask) delete; SimpleTask operator(const SimpleTask) delete; // 恢复协程执行 void resume() { if (coro_handle !coro_handle.done()) { coro_handle.resume(); } } // 查询协程是否已执行完毕 bool is_done() const noexcept { return !coro_handle || coro_handle.done(); } private: std::coroutine_handlePromiseType coro_handle; }; // 实现 PromiseType::get_return_object inline SimpleTask PromiseType::get_return_object() noexcept { // 通过当前协程的句柄来构造 SimpleTask return SimpleTask{std::coroutine_handlePromiseType::from_promise(*this)}; }3.3 实现一个可等待的“睡眠”操作SleepAwaiter现在我们来创建一个可以让协程“睡眠”指定时间的Awaiter。这需要与一个简单的调度器配合。// 睡眠等待器 struct SleepAwaiter { std::chrono::milliseconds duration; // 一个简单的全局调度器指针实际应用应有更完善的设计 class Scheduler* scheduler; SleepAwaiter(std::chrono::milliseconds dur, class Scheduler* sched) : duration(dur), scheduler(sched) {} // 永远返回 false因为我们总是需要异步等待 bool await_ready() const noexcept { return false; } // 核心安排一个定时恢复操作 void await_suspend(std::coroutine_handle handle) const; // 睡眠完成恢复执行无返回值 void await_resume() const noexcept {} };3.4 实现一个简易调度器Scheduler调度器负责在指定的时间后恢复被挂起的协程。这里用一个单线程循环来模拟。// 简易调度器 class Scheduler { public: struct ScheduledTask { std::chrono::steady_clock::time_point resume_time; std::coroutine_handle handle; // 用于优先队列排序时间早的优先 bool operator(const ScheduledTask other) const { return resume_time other.resume_time; } }; void schedule_after(std::chrono::milliseconds delay, std::coroutine_handle handle) { auto time_point std::chrono::steady_clock::now() delay; std::lock_guard lock(mtx); tasks.emplace(ScheduledTask{time_point, handle}); cv.notify_one(); } void run() { running true; while (running || !tasks.empty()) { std::unique_lock lock(mtx); if (tasks.empty()) { cv.wait(lock); continue; } auto next_task tasks.top(); if (next_task.resume_time std::chrono::steady_clock::now()) { tasks.pop(); lock.unlock(); // 在恢复协程前释放锁避免死锁 next_task.handle.resume(); // 恢复协程执行 } else { cv.wait_until(lock, next_task.resume_time); } } } void stop() { running false; cv.notify_all(); } private: std::priority_queueScheduledTask, std::vectorScheduledTask, std::greater tasks; std::mutex mtx; std::condition_variable cv; std::atomicbool running{false}; }; // 实现 SleepAwaiter::await_suspend void SleepAwaiter::await_suspend(std::coroutine_handle handle) const { if (scheduler) { scheduler-schedule_after(duration, handle); } // 如果没有调度器这里应该抛出异常或处理错误 }3.5 封装用户友好的sleep_for函数为了让协程函数里能方便地使用co_await sleep_for(100ms)我们提供一个辅助函数。// 全局调度器实例简单起见用全局变量生产环境应用依赖注入等方式 inline Scheduler global_scheduler; SleepAwaiter sleep_for(std::chrono::milliseconds ms) { return SleepAwaiter{ms, global_scheduler}; }3.6 编写并运行第一个协程任务万事俱备现在可以写一个协程函数了。// 一个协程函数示例 SimpleTask example_coroutine() { std::cout Task started at thread: std::this_thread::get_id() std::endl; std::cout Sleeping for 1 second... std::endl; co_await sleep_for(std::chrono::seconds(1)); // 协程在此挂起 std::cout Awake! Now at thread: std::this_thread::get_id() std::endl; // 可能在调度器线程恢复 std::cout Sleeping for 500ms... std::endl; co_await sleep_for(std::chrono::milliseconds(500)); std::cout Done! std::endl; // 隐式 co_return void; } int main() { // 启动调度器线程 std::thread scheduler_thread([] { global_scheduler.run(); }); // 创建协程任务。由于 initial_suspend 返回 suspend_always此时协程是挂起状态。 auto task example_coroutine(); // 手动恢复协程开始执行 task.resume(); // 主线程等待一段时间让协程任务有机会执行 std::this_thread::sleep_for(std::chrono::seconds(3)); // 停止调度器 global_scheduler.stop(); scheduler_thread.join(); // task 析构时如果协程已结束会销毁协程帧 return 0; }编译这个程序需要支持C20的编译器如GCC 11, Clang 14, MSVC 19.28并开启对应标志例如-stdc20。运行结果可能类似于Task started at thread: 140735745722240 (主线程ID) Sleeping for 1 second... Awake! Now at thread: 123145551810560 (调度器线程ID) Sleeping for 500ms... Done!实操心得 注意观察线程ID的变化。co_await sleep_for之后的代码是在调度器线程中恢复执行的。这意味着协程挂起时不会阻塞调用者线程这里是主线程调用者线程可以去做其他工作这正是异步并发的威力。同时这也要求我们注意线程安全性协程内访问共享数据可能需要同步。4. 深入与异步I/O库集成以模拟为例纯粹的SleepAwaiter展示了机制但真实场景更多是co_await一个套接字读写操作。我们基于上面的框架模拟一个异步读操作AsyncReadAwaiter。假设我们有一个虚构的异步I/O对象AsyncSocket。struct AsyncSocket { // 模拟异步读操作返回一个可等待对象 AsyncReadAwaiter async_read_some(char* buffer, size_t size) { return AsyncReadAwaiter{this, buffer, size, global_scheduler}; } // 模拟底层收到数据由网络层驱动 void on_data_received(size_t bytes_transferred) { // ... 触发等待器完成 ... } }; struct AsyncReadAwaiter { AsyncSocket* socket; char* buffer; size_t size; Scheduler* scheduler; size_t bytes_read; // 结果 std::coroutine_handle stored_handle; // 暂存要恢复的句柄 bool await_ready() const noexcept { return false; /* 总是异步 */ } void await_suspend(std::coroutine_handle handle) { stored_handle handle; // 关键将恢复句柄与socket的读事件绑定 // 这里只是模拟真实库会将 handle 注册到 epoll/kqueue/IOCP 等系统机制 socket-register_read_callback([this, handle](size_t bytes) mutable { this-bytes_read bytes; // 将恢复操作提交给调度器确保在正确的上下文执行 scheduler-schedule_immediately(handle); }); // 启动实际的异步读操作如调用 read(fd, ... MSG_DONTWAIT) 并注册到事件循环 socket-start_async_read(buffer, size); } size_t await_resume() const noexcept { return bytes_read; // 将读取的字节数作为 co_await 的结果返回 } };在协程中使用它SimpleTask echo_session(AsyncSocket socket) { char buf[1024]; try { while (true) { // 代码是顺序的但执行是异步的 size_t n co_await socket.async_read_some(buf, sizeof(buf)); std::cout Received n bytes. std::endl; // co_await async_write(...); } } catch (const std::exception e) { std::cerr Session error: e.what() std::endl; } }这个模式就是boost::asio等库提供co_await支持的方式。库作者负责为各种异步操作连接、读、写、定时器实现对应的Awaitable类型业务开发者就能用同步风格编写异步代码。5. 常见陷阱、调试技巧与性能考量C20协程强大但不易驾驭下面是我在实际项目中总结的一些关键点。5.1 内存泄漏协程帧的销毁这是新手最容易犯的错误。协程帧不会自动销毁。谁创建谁销毁通常协程帧的销毁责任在于promise_type::final_suspend()的返回值和最终获取到的句柄的调用者。常见模式final_suspend()返回std::suspend_always。这意味着协程结束后会暂停在最终挂起点。此时协程句柄仍然有效但handle.done()为true。你必须手动调用handle.destroy()来释放内存。我们的SimpleTask在析构函数中就是这样做的。final_suspend()返回std::suspend_never。协程结束后会自动销毁自身帧。此时你持有的句柄立即失效handle.done()可能为true但访问它已非法。这种模式更安全但意味着你无法在协程结束后从promise对象中获取最终结果因为对象已被销毁。建议 对于返回Task对象的协程采用模式1并在Task的析构函数中检查并销毁。使用RAII管理生命周期。5.2 悬空引用与指针协程挂起时其局部变量和参数的生命周期与协程帧绑定。如果你在await_suspend中捕获了协程局部变量的引用或指针并在协程恢复之前甚至之后使用它将导致未定义行为。// 危险代码示例 SomeAwaiter bad_awaiter(std::string local_str) { // 捕获了局部变量的引用 return SomeAwaiter{local_str}; } SimpleTask risky_coroutine() { std::string local hello; co_await bad_awaiter(local); // 协程挂起local 的引用被传递出去 // 如果 bad_awaiter 在协程恢复前使用了那个指针而协程可能已被移动到其他帧危险 }解决方案 在Awaiter中按值存储所需数据或存储std::shared_ptr到堆上分配的数据。5.3 调试困难协程的挂起和恢复打乱了传统的调用栈使得调试器中的调用栈信息变得难以理解。你可能会看到一堆编译器生成的状态机函数名。技巧1 使用支持协程调试的较新版本GDB或Visual Studio。技巧2 在关键位置添加日志打印协程句柄地址或自定义的协程ID跟踪其生命周期和状态流转。技巧3 简化问题。先在一个最小可复现的例子里验证协程逻辑是否正确。5.4 性能考量协程帧分配 每次调用协程函数都可能有一次堆分配。对于高频短小的协程这可能是瓶颈。解决方案 使用有状态的自定义分配器如内存池并通过promise_type::operator new和operator delete重载来定制内存分配。类型擦除与间接调用 像std::function或虚函数接口包装协程句柄可能会引入间接调用开销。在极致性能场景下需要考虑。与现有线程池/事件循环集成 确保你的Awaiter在await_suspend中将恢复操作派发到正确的执行器Executor上避免在错误的线程上下文访问非线程安全的数据。5.5 编译器支持与标准库完善度C20协程是一个比较新的特性各编译器支持程度和标准库配套工具仍在完善中。MSVC 目前对协程的支持相对最成熟、最友好。GCC/Clang 需要较新版本如GCC 11 Clang 14且一些边缘情况或调试体验可能不如MSVC。标准库工具std::generator(C23) 等更上层的协程工具正在加入标准库但目前C20你需要自己搭建或依赖第三方库如cppcoro来获得generator,task,sync_wait等常用组件。6. 进阶实现一个简单的Generatorco_yield用于实现生成器Generator它允许协程产生一系列值。我们来快速实现一个IntRangeGenerator。templatetypename T struct Generator { struct promise_type { T current_value; // 存储 yield 的值 Generator get_return_object() { return Generator{std::coroutine_handlepromise_type::from_promise(*this)}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() { std::rethrow_exception(std::current_exception()); } void return_void() noexcept {} // 处理 co_yield value std::suspend_always yield_value(T value) noexcept { current_value std::move(value); return {}; // 总是挂起让调用者来取走值 } }; std::coroutine_handlepromise_type handle; explicit Generator(std::coroutine_handlepromise_type h) : handle(h) {} ~Generator() { if (handle) handle.destroy(); } // 移动操作 Generator(Generator other) noexcept : handle(std::exchange(other.handle, nullptr)) {} Generator operator(Generator other) noexcept { /* ... */ } // 迭代器接口 struct sentinel {}; struct iterator { std::coroutine_handlepromise_type handle; bool operator!(sentinel) const { return !handle.done(); } iterator operator() { handle.resume(); return *this; } const T operator*() const { return handle.promise().current_value; } }; iterator begin() { if (handle) handle.resume(); return iterator{handle}; } sentinel end() { return {}; } }; // 使用示例 Generatorint range(int start, int end) { for (int i start; i end; i) { co_yield i; // 每次 yield 挂起并返回当前 i 的值 } } int main() { for (int num : range(1, 5)) { std::cout num ; // 输出: 1 2 3 4 } std::cout std::endl; return 0; }这个Generator实现了基于范围的for循环接口使得协程产生的值可以像容器一样被遍历非常优雅。C20协程是一把锋利的双刃剑。它彻底改变了C异步编程的代码面貌将开发者从回调地狱中解放出来但同时也带来了更高的理解门槛和更复杂的底层机制。我的建议是对于新项目如果团队技术栈允许编译器版本够新可以积极尝试采用协程来编写异步逻辑优先考虑使用成熟的协程库如cppcoro或boost::asio的协程接口来规避底层细节。对于现有项目可以在性能关键或逻辑复杂的异步模块中进行局部重构。理解其原理善用其优势你就能在C高性能编程的道路上更进一步。
C++20协程实战:从原理到异步任务框架实现
1. 项目概述为什么我们需要C协程如果你写过C的网络服务或者任何涉及I/O密集型操作的代码大概率对“回调地狱”这个词深有体会。异步编程这个旨在提升程序并发能力和资源利用率的利器在传统的基于回调或Future/Promise的模型下代码结构往往会变得支离破碎逻辑跳转让人眼花缭乱。一个简单的“连接-发送-接收”流程可能就需要嵌套五六层回调函数调试起来如同在迷宫里找出口。这正是“C协程实战异步编程代码实现”这个项目要解决的核心痛点。C20标准正式引入的协程Coroutines特性为我们提供了一种全新的、更符合人类线性思维的方式来编写异步代码。它允许你将一个可能被挂起和恢复的函数写成顺序执行的样子底层由编译器和运行时库来处理复杂的状态保存与恢复。简单来说就是用写同步代码的直观性获得异步代码的高性能。这不仅仅是语法糖而是一种编程范式的转变。它尤其适合网络编程、文件I/O、定时任务调度等场景。通过本项目我将带你从零开始手把手实现一个基于C20协程的简易异步任务框架并深入剖析其背后的原理。无论你是想优化现有项目的异步架构还是单纯对这门“黑魔法”感到好奇这篇实战指南都将为你提供可直接复现的代码和踩坑经验。2. 协程核心概念与C20实现机制拆解在动手写代码之前我们必须先理解C20协程的几个核心“零件”。很多教程一上来就抛出一堆术语让人望而却步。我这里用一个更生活化的比喻来解释你可以把协程想象成一个可以随时暂停和继续播放的视频播放器。2.1 核心组件角色扮演协程函数 (Coroutine Function) 就是那个“视频内容”。任何包含co_await,co_yield,co_return关键字的函数都会被编译器特殊处理变成一个协程。它定义了主要的执行逻辑。承诺类型 (Promise Type) 这是协程的“制片人”和“导演”。它负责协程的“生命周期管理”和“产出物管理”。具体来说它要定义协程的返回值对象get_return_object。初始挂起点initial_suspend协程一开场是直接开拍还是先暂停最终挂起点final_suspend协程拍完了co_return或函数体结束后是自动清理片场销毁协程帧还是暂停等待别人来清理如何处理co_yield产生的值yield_value。如何处理未捕获的异常unhandled_exception。协程句柄 (coroutine_handle) 这是“播放器的遥控器”。通过它外部代码可以恢复resume()或销毁destroy()一个被挂起的协程。它也是访问协程内部状态比如promise对象的入口。等待体与等待器 (Awaitable Awaiter) 这是决定“何时暂停播放”的规则。co_await expr中的expr必须是一个可等待体 (Awaitable)。编译器会尝试从中获取一个等待器 (Awaiter)。等待器有三个关键方法await_ready() 问“视频下一段缓冲好了吗”。如果返回true就不暂停直接继续播。await_suspend(coroutine_handle) 如果没缓冲好await_ready()返回false就在这里注册回调“等缓冲好了就用这个遥控器句柄恢复播放”。然后当前协程暂停。await_resume() 当协程被恢复后这个方法的返回值就是co_await表达式的结果。2.2 编译器背后的魔法协程帧当你调用一个协程函数时编译器并不会像普通函数那样直接在调用栈上分配空间。它会在堆上或通过定制分配器分配一块称为协程帧 (Coroutine Frame)的内存。这块内存里存放了传递给协程的参数。协程函数体内的局部变量包括编译器生成的隐藏状态变量。promise对象。当前挂起点的位置一个恢复地址。其他必要的簿记信息。这正是“无栈协程”名称的由来——它的状态保存在堆上独立的帧里而不是传统的函数调用栈上。挂起时保存当前执行点恢复时从保存的点继续执行并恢复所有局部变量的值。这个转换过程由编译器自动完成通常是通过将函数体转换成一个包含巨大switch语句的状态机来实现的。注意 协程帧的分配可能带来动态内存开销这是性能考量的一个点。高性能库通常会提供自定义分配器如内存池来优化这一点。2.3co_await的工作流程理解co_await的流程是理解协程异步操作的关键。当执行到co_await expr;时获取等待器 通过expr获取awaiter对象可能经过operator co_await转换。询问就绪 调用awaiter.await_ready()。若为true跳至步骤5。若为false继续步骤3。挂起协程 挂起当前协程保存所有局部变量和当前执行位置到协程帧。安排恢复 调用awaiter.await_suspend(handle)。这是异步操作的核心在这个方法里你通常会将恢复协程的句柄handle注册到某个异步操作的回调中例如将handle交给epoll或IOCP当数据可读时调用handle.resume()。然后函数返回控制权交还给调用者或调度器。恢复与取值 当异步操作完成协程被恢复通过handle.resume()后执行awaiter.await_resume()其返回值作为co_await表达式的结果然后继续执行后续代码。3. 实战从零构建一个简易协程任务框架理论说得再多不如一行代码。我们现在就来构建一个最简单的协程任务框架SimpleTask。这个框架不依赖boost::asio等大型库旨在揭示最纯粹的C20协程机制。我们将实现一个可以co_await的“睡眠”定时器任务。3.1 定义承诺类型和任务返回值首先我们需要定义协程的返回类型SimpleTask和其内部的承诺类型PromiseType。#include coroutine #include iostream #include chrono #include thread #include functional #include queue #include atomic #include memory // 前向声明 struct SimpleTask; // 承诺类型 struct PromiseType { // 1. 获取返回值对象一个包装了本承诺的 SimpleTask SimpleTask get_return_object() noexcept; // 2. 初始挂起策略协程开始即暂停让调用者决定何时启动 std::suspend_always initial_suspend() noexcept { return {}; } // 3. 最终挂起策略协程结束后也暂停我们需要手动销毁协程帧 std::suspend_always final_suspend() noexcept { return {}; } // 4. 没有返回值本例中任务不产生值只表示完成 void return_void() noexcept {} // 5. 异常处理简单打印并重新抛出实际项目需更健壮 void unhandled_exception() { std::rethrow_exception(std::current_exception()); } };std::suspend_always和std::suspend_never是标准库提供的两个最简单的Awaiter它们只实现await_ready分别返回false和true和await_suspend空操作。3.2 定义任务类型SimpleTaskSimpleTask是协程对外的接口它持有一个协程句柄。struct SimpleTask { // 别名让编译器知道我们的承诺类型 using promise_type PromiseType; // 构造函数接收一个协程句柄 explicit SimpleTask(std::coroutine_handlePromiseType handle) noexcept : coro_handle(handle) {} // 析构时如果协程句柄有效且协程未结束需要销毁它防止内存泄漏 ~SimpleTask() { if (coro_handle coro_handle.done()) { coro_handle.destroy(); } } // 移动构造/赋值管理句柄所有权 SimpleTask(SimpleTask other) noexcept : coro_handle(std::exchange(other.coro_handle, nullptr)) {} SimpleTask operator(SimpleTask other) noexcept { if (this ! other) { if (coro_handle coro_handle.done()) { coro_handle.destroy(); } coro_handle std::exchange(other.coro_handle, nullptr); } return *this; } // 删除拷贝句柄资源唯一 SimpleTask(const SimpleTask) delete; SimpleTask operator(const SimpleTask) delete; // 恢复协程执行 void resume() { if (coro_handle !coro_handle.done()) { coro_handle.resume(); } } // 查询协程是否已执行完毕 bool is_done() const noexcept { return !coro_handle || coro_handle.done(); } private: std::coroutine_handlePromiseType coro_handle; }; // 实现 PromiseType::get_return_object inline SimpleTask PromiseType::get_return_object() noexcept { // 通过当前协程的句柄来构造 SimpleTask return SimpleTask{std::coroutine_handlePromiseType::from_promise(*this)}; }3.3 实现一个可等待的“睡眠”操作SleepAwaiter现在我们来创建一个可以让协程“睡眠”指定时间的Awaiter。这需要与一个简单的调度器配合。// 睡眠等待器 struct SleepAwaiter { std::chrono::milliseconds duration; // 一个简单的全局调度器指针实际应用应有更完善的设计 class Scheduler* scheduler; SleepAwaiter(std::chrono::milliseconds dur, class Scheduler* sched) : duration(dur), scheduler(sched) {} // 永远返回 false因为我们总是需要异步等待 bool await_ready() const noexcept { return false; } // 核心安排一个定时恢复操作 void await_suspend(std::coroutine_handle handle) const; // 睡眠完成恢复执行无返回值 void await_resume() const noexcept {} };3.4 实现一个简易调度器Scheduler调度器负责在指定的时间后恢复被挂起的协程。这里用一个单线程循环来模拟。// 简易调度器 class Scheduler { public: struct ScheduledTask { std::chrono::steady_clock::time_point resume_time; std::coroutine_handle handle; // 用于优先队列排序时间早的优先 bool operator(const ScheduledTask other) const { return resume_time other.resume_time; } }; void schedule_after(std::chrono::milliseconds delay, std::coroutine_handle handle) { auto time_point std::chrono::steady_clock::now() delay; std::lock_guard lock(mtx); tasks.emplace(ScheduledTask{time_point, handle}); cv.notify_one(); } void run() { running true; while (running || !tasks.empty()) { std::unique_lock lock(mtx); if (tasks.empty()) { cv.wait(lock); continue; } auto next_task tasks.top(); if (next_task.resume_time std::chrono::steady_clock::now()) { tasks.pop(); lock.unlock(); // 在恢复协程前释放锁避免死锁 next_task.handle.resume(); // 恢复协程执行 } else { cv.wait_until(lock, next_task.resume_time); } } } void stop() { running false; cv.notify_all(); } private: std::priority_queueScheduledTask, std::vectorScheduledTask, std::greater tasks; std::mutex mtx; std::condition_variable cv; std::atomicbool running{false}; }; // 实现 SleepAwaiter::await_suspend void SleepAwaiter::await_suspend(std::coroutine_handle handle) const { if (scheduler) { scheduler-schedule_after(duration, handle); } // 如果没有调度器这里应该抛出异常或处理错误 }3.5 封装用户友好的sleep_for函数为了让协程函数里能方便地使用co_await sleep_for(100ms)我们提供一个辅助函数。// 全局调度器实例简单起见用全局变量生产环境应用依赖注入等方式 inline Scheduler global_scheduler; SleepAwaiter sleep_for(std::chrono::milliseconds ms) { return SleepAwaiter{ms, global_scheduler}; }3.6 编写并运行第一个协程任务万事俱备现在可以写一个协程函数了。// 一个协程函数示例 SimpleTask example_coroutine() { std::cout Task started at thread: std::this_thread::get_id() std::endl; std::cout Sleeping for 1 second... std::endl; co_await sleep_for(std::chrono::seconds(1)); // 协程在此挂起 std::cout Awake! Now at thread: std::this_thread::get_id() std::endl; // 可能在调度器线程恢复 std::cout Sleeping for 500ms... std::endl; co_await sleep_for(std::chrono::milliseconds(500)); std::cout Done! std::endl; // 隐式 co_return void; } int main() { // 启动调度器线程 std::thread scheduler_thread([] { global_scheduler.run(); }); // 创建协程任务。由于 initial_suspend 返回 suspend_always此时协程是挂起状态。 auto task example_coroutine(); // 手动恢复协程开始执行 task.resume(); // 主线程等待一段时间让协程任务有机会执行 std::this_thread::sleep_for(std::chrono::seconds(3)); // 停止调度器 global_scheduler.stop(); scheduler_thread.join(); // task 析构时如果协程已结束会销毁协程帧 return 0; }编译这个程序需要支持C20的编译器如GCC 11, Clang 14, MSVC 19.28并开启对应标志例如-stdc20。运行结果可能类似于Task started at thread: 140735745722240 (主线程ID) Sleeping for 1 second... Awake! Now at thread: 123145551810560 (调度器线程ID) Sleeping for 500ms... Done!实操心得 注意观察线程ID的变化。co_await sleep_for之后的代码是在调度器线程中恢复执行的。这意味着协程挂起时不会阻塞调用者线程这里是主线程调用者线程可以去做其他工作这正是异步并发的威力。同时这也要求我们注意线程安全性协程内访问共享数据可能需要同步。4. 深入与异步I/O库集成以模拟为例纯粹的SleepAwaiter展示了机制但真实场景更多是co_await一个套接字读写操作。我们基于上面的框架模拟一个异步读操作AsyncReadAwaiter。假设我们有一个虚构的异步I/O对象AsyncSocket。struct AsyncSocket { // 模拟异步读操作返回一个可等待对象 AsyncReadAwaiter async_read_some(char* buffer, size_t size) { return AsyncReadAwaiter{this, buffer, size, global_scheduler}; } // 模拟底层收到数据由网络层驱动 void on_data_received(size_t bytes_transferred) { // ... 触发等待器完成 ... } }; struct AsyncReadAwaiter { AsyncSocket* socket; char* buffer; size_t size; Scheduler* scheduler; size_t bytes_read; // 结果 std::coroutine_handle stored_handle; // 暂存要恢复的句柄 bool await_ready() const noexcept { return false; /* 总是异步 */ } void await_suspend(std::coroutine_handle handle) { stored_handle handle; // 关键将恢复句柄与socket的读事件绑定 // 这里只是模拟真实库会将 handle 注册到 epoll/kqueue/IOCP 等系统机制 socket-register_read_callback([this, handle](size_t bytes) mutable { this-bytes_read bytes; // 将恢复操作提交给调度器确保在正确的上下文执行 scheduler-schedule_immediately(handle); }); // 启动实际的异步读操作如调用 read(fd, ... MSG_DONTWAIT) 并注册到事件循环 socket-start_async_read(buffer, size); } size_t await_resume() const noexcept { return bytes_read; // 将读取的字节数作为 co_await 的结果返回 } };在协程中使用它SimpleTask echo_session(AsyncSocket socket) { char buf[1024]; try { while (true) { // 代码是顺序的但执行是异步的 size_t n co_await socket.async_read_some(buf, sizeof(buf)); std::cout Received n bytes. std::endl; // co_await async_write(...); } } catch (const std::exception e) { std::cerr Session error: e.what() std::endl; } }这个模式就是boost::asio等库提供co_await支持的方式。库作者负责为各种异步操作连接、读、写、定时器实现对应的Awaitable类型业务开发者就能用同步风格编写异步代码。5. 常见陷阱、调试技巧与性能考量C20协程强大但不易驾驭下面是我在实际项目中总结的一些关键点。5.1 内存泄漏协程帧的销毁这是新手最容易犯的错误。协程帧不会自动销毁。谁创建谁销毁通常协程帧的销毁责任在于promise_type::final_suspend()的返回值和最终获取到的句柄的调用者。常见模式final_suspend()返回std::suspend_always。这意味着协程结束后会暂停在最终挂起点。此时协程句柄仍然有效但handle.done()为true。你必须手动调用handle.destroy()来释放内存。我们的SimpleTask在析构函数中就是这样做的。final_suspend()返回std::suspend_never。协程结束后会自动销毁自身帧。此时你持有的句柄立即失效handle.done()可能为true但访问它已非法。这种模式更安全但意味着你无法在协程结束后从promise对象中获取最终结果因为对象已被销毁。建议 对于返回Task对象的协程采用模式1并在Task的析构函数中检查并销毁。使用RAII管理生命周期。5.2 悬空引用与指针协程挂起时其局部变量和参数的生命周期与协程帧绑定。如果你在await_suspend中捕获了协程局部变量的引用或指针并在协程恢复之前甚至之后使用它将导致未定义行为。// 危险代码示例 SomeAwaiter bad_awaiter(std::string local_str) { // 捕获了局部变量的引用 return SomeAwaiter{local_str}; } SimpleTask risky_coroutine() { std::string local hello; co_await bad_awaiter(local); // 协程挂起local 的引用被传递出去 // 如果 bad_awaiter 在协程恢复前使用了那个指针而协程可能已被移动到其他帧危险 }解决方案 在Awaiter中按值存储所需数据或存储std::shared_ptr到堆上分配的数据。5.3 调试困难协程的挂起和恢复打乱了传统的调用栈使得调试器中的调用栈信息变得难以理解。你可能会看到一堆编译器生成的状态机函数名。技巧1 使用支持协程调试的较新版本GDB或Visual Studio。技巧2 在关键位置添加日志打印协程句柄地址或自定义的协程ID跟踪其生命周期和状态流转。技巧3 简化问题。先在一个最小可复现的例子里验证协程逻辑是否正确。5.4 性能考量协程帧分配 每次调用协程函数都可能有一次堆分配。对于高频短小的协程这可能是瓶颈。解决方案 使用有状态的自定义分配器如内存池并通过promise_type::operator new和operator delete重载来定制内存分配。类型擦除与间接调用 像std::function或虚函数接口包装协程句柄可能会引入间接调用开销。在极致性能场景下需要考虑。与现有线程池/事件循环集成 确保你的Awaiter在await_suspend中将恢复操作派发到正确的执行器Executor上避免在错误的线程上下文访问非线程安全的数据。5.5 编译器支持与标准库完善度C20协程是一个比较新的特性各编译器支持程度和标准库配套工具仍在完善中。MSVC 目前对协程的支持相对最成熟、最友好。GCC/Clang 需要较新版本如GCC 11 Clang 14且一些边缘情况或调试体验可能不如MSVC。标准库工具std::generator(C23) 等更上层的协程工具正在加入标准库但目前C20你需要自己搭建或依赖第三方库如cppcoro来获得generator,task,sync_wait等常用组件。6. 进阶实现一个简单的Generatorco_yield用于实现生成器Generator它允许协程产生一系列值。我们来快速实现一个IntRangeGenerator。templatetypename T struct Generator { struct promise_type { T current_value; // 存储 yield 的值 Generator get_return_object() { return Generator{std::coroutine_handlepromise_type::from_promise(*this)}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() { std::rethrow_exception(std::current_exception()); } void return_void() noexcept {} // 处理 co_yield value std::suspend_always yield_value(T value) noexcept { current_value std::move(value); return {}; // 总是挂起让调用者来取走值 } }; std::coroutine_handlepromise_type handle; explicit Generator(std::coroutine_handlepromise_type h) : handle(h) {} ~Generator() { if (handle) handle.destroy(); } // 移动操作 Generator(Generator other) noexcept : handle(std::exchange(other.handle, nullptr)) {} Generator operator(Generator other) noexcept { /* ... */ } // 迭代器接口 struct sentinel {}; struct iterator { std::coroutine_handlepromise_type handle; bool operator!(sentinel) const { return !handle.done(); } iterator operator() { handle.resume(); return *this; } const T operator*() const { return handle.promise().current_value; } }; iterator begin() { if (handle) handle.resume(); return iterator{handle}; } sentinel end() { return {}; } }; // 使用示例 Generatorint range(int start, int end) { for (int i start; i end; i) { co_yield i; // 每次 yield 挂起并返回当前 i 的值 } } int main() { for (int num : range(1, 5)) { std::cout num ; // 输出: 1 2 3 4 } std::cout std::endl; return 0; }这个Generator实现了基于范围的for循环接口使得协程产生的值可以像容器一样被遍历非常优雅。C20协程是一把锋利的双刃剑。它彻底改变了C异步编程的代码面貌将开发者从回调地狱中解放出来但同时也带来了更高的理解门槛和更复杂的底层机制。我的建议是对于新项目如果团队技术栈允许编译器版本够新可以积极尝试采用协程来编写异步逻辑优先考虑使用成熟的协程库如cppcoro或boost::asio的协程接口来规避底层细节。对于现有项目可以在性能关键或逻辑复杂的异步模块中进行局部重构。理解其原理善用其优势你就能在C高性能编程的道路上更进一步。