C++20协程实战:从原理到实现异步编程与生成器

1. 从“线程”到“协程”:为什么我们需要另一种并发模型?

如果你写过C++的多线程程序,大概率对std::thread、互斥锁std::mutex和条件变量std::condition_variable这套组合拳又爱又恨。爱的是,它们确实能榨干多核CPU的性能;恨的是,为了处理数据竞争、死锁和线程同步,代码复杂度直线上升,调试起来更是噩梦。一个简单的生产者-消费者模型,代码里可能一半都在处理锁和等待。这就像你要开一家小餐馆,却不得不先雇佣一个庞大的管理团队来处理内部协调,大部分精力没用在炒菜上。

协程(Coroutines)的出现,就是为了解决这种“重型”并发带来的心智负担。它不是要取代线程,而是提供一种更轻量、更符合人类线性思维方式的异步编程模型。你可以把线程想象成真正的“工人”,操作系统负责调度他们,切换他们(上下文切换)成本很高,而且工人之间沟通(线程间通信)很麻烦。而协程更像是这个工人自己掌握的“多重任务处理技巧”,他在切菜的时候,如果遇到要等水烧开,可以自己先记下切菜的进度,转身去处理配菜,等水开了再回来继续。这个“记下进度并切换”的过程,就是协程的挂起(suspend)和恢复(resume),完全在用户态进行,开销极低。

C++20正式将协程作为语言特性引入,这不是一个库,而是编译器直接支持的原生语法。这意味着我们不再需要依赖第三方库(比如Boost.Coroutine)那套复杂晦涩的接口,可以用更直观的关键字(co_await,co_yield,co_return)来编写异步代码。它的核心价值在于,用同步代码的书写风格,获得了异步程序的性能。对于I/O密集型应用(网络服务器、文件处理、游戏逻辑)来说,这是巨大的福音。你不再需要写层层嵌套的回调函数(Callback Hell),也不需要理解复杂的Promise/Future链,代码可读性和可维护性得到了质的提升。

2. C++20协程的核心机制:三巨头与承诺对象

要理解C++协程,必须吃透三个核心关键字和它们背后的“承诺对象”(Promise Object)。这就像是协程世界的宪法,规定了协程如何启动、如何交互以及如何结束。

2.1 协程的三把钥匙:co_await,co_yield,co_return

co_await:这是异步等待的基石。当你在协程函数里写下auto result = co_await some_async_task();时,意味着:“我要等这个some_async_task()完成,在它完成之前,我可以先被挂起,把执行权交出去,等它有结果了再唤醒我继续执行,并把结果给我。” 这个表达式必须作用于一个“可等待体”(Awaitable)。编译器会生成代码,在挂起前保存当前函数栈帧(局部变量状态)的所有必要信息,这个保存下来的状态称为“协程帧”(coroutine frame),它通常分配在堆上。

co_yield:用于生成一个值并挂起协程,通常用于生成器(Generator)。比如co_yield i++;会把当前的i值传递给调用者,然后协程挂起。下次恢复时,从co_yield之后继续执行。这完美实现了惰性求值序列,比如无限斐波那契数列生成器,内存占用是常数,而用容器存储则是无限的。

co_return:用于结束协程并返回一个最终值(或void)。它和普通return不同,它通过承诺对象来传递返回值,而不是直接返回给调用者。

2.2 幕后导演:承诺对象与协程句柄

每一个协程函数,编译器都会为其秘密创建一个“承诺对象”(Promise Object)。这个对象是协程内部状态的管理者,生命周期与协程帧一致。它的类型是由协程函数的返回类型决定的,准确说,是由返回类型上定义的promise_type嵌套类型决定的。

承诺对象负责几件大事:

  1. 创建返回值:通过get_return_object()方法,返回给协程外部调用者的那个对象(通常这个对象会持有一个“协程句柄”)。
  2. 初始/最终挂起initial_suspend()final_suspend()方法决定协程一开始是否直接挂起,以及结束后是否挂起。这对于控制协程的自动启动和自动销毁至关重要。
  3. 处理返回值/异常return_value()return_void()处理co_return的值;unhandled_exception()处理协程内部未捕获的异常。
  4. 提供可等待体await_transform()方法可以定制对co_await表达式的处理。

协程句柄std::coroutine_handle)是一个不拥有协程帧所有权的“指针”,用于从外部恢复或销毁一个挂起的协程。你可以通过promise()方法获取到其关联的承诺对象。调用handle.resume()恢复协程执行,调用handle.destroy()销毁协程帧并释放内存。

关键理解:协程函数本身的“返回值”(比如Generator),并不是协程内部计算出的那个值。它只是一个给外部调用者的“控制器”,内部通过co_yieldco_return产生的值,是通过承诺对象和协程句柄这个通道传递出来的。这是初学者最容易混淆的点。

2.3 编译器视角下的协程变换

当你定义一个返回类型为Task的协程函数时:

Task<int> foo_coroutine() { int a = co_await some_async_work(); co_return a * 2; }

编译器会把它重写成一个状态机,大致结构如下(概念上):

  1. 在堆上分配协程帧,包含承诺对象、所有局部变量(如a)、当前挂起点(一个整数标签)等。
  2. 构造承诺对象,调用promise.get_return_object()得到Task对象返回给调用者。
  3. 调用promise.initial_suspend()co_await其结果,决定是否立即挂起。
  4. 根据挂起点标签,跳转到不同的代码块执行。遇到co_await,先计算可等待体,执行其await_ready,await_suspend,await_resume方法。
  5. 遇到co_return,调用promise.return_value(...),然后执行final_suspend
  6. 协程结束,控制权返回给恢复者或销毁者。

3. 从零手写一个可用的协程类型:生成器与异步任务

理论说得再多,不如动手写一个。我们来实现两个最常用的协程类型:生成器(Generator)和异步任务(Task)。这将彻底揭开协程的神秘面纱。

3.1 实现一个简单的生成器(Generator)

生成器用于产生一个惰性的值序列。我们的目标是实现一个Generator,可以这样用:

Generator<int> range(int start, int end) { for (int i = start; i < end; ++i) { co_yield i; // 产生一个值,并挂起 } // 协程结束,自动销毁 } int main() { for (auto val : range(1, 10)) { // Generator 需要支持 begin()/end() std::cout << val << ' '; } // 输出: 1 2 3 4 5 6 7 8 9 }

下面是Generator的核心实现:

#include <coroutine> #include <exception> #include <iostream> template<typename T> struct Generator { // 承诺类型定义,必须嵌套在返回类型中 struct promise_type { T current_value; // 存储 yield 出来的值 std::exception_ptr exception; // 存储异常 Generator get_return_object() { // 使用 coroutine_handle::from_promise 从承诺对象获取句柄 return Generator{std::coroutine_handle<promise_type>::from_promise(*this)}; } // 初始不挂起,让协程立即执行直到第一个 yield std::suspend_never initial_suspend() noexcept { return {}; } // 最终挂起,让外部有机会在协程结束前读取最后的值或处理异常 std::suspend_always final_suspend() noexcept { return {}; } // 处理 co_yield 表达式 std::suspend_always yield_value(T value) { current_value = std::move(value); return {}; // 返回一个总是挂起的 awaitable } // 如果协程没有 co_return,需要此函数 void return_void() noexcept {} // 存储异常 void unhandled_exception() { exception = std::current_exception(); } }; // 迭代器,用于支持 range-based for struct iterator { std::coroutine_handle<promise_type> handle; bool done; iterator(std::coroutine_handle<promise_type> h, bool d) : handle(h), done(d) {} T& operator*() const { return handle.promise().current_value; } iterator& operator++() { if (!done) { handle.resume(); // 恢复协程,执行到下一个 yield 或结束 done = handle.done(); } return *this; } bool operator!=(const iterator& other) const { return done != other.done; } }; iterator begin() { if (handle) { handle.resume(); // 启动协程到第一个挂起点 if (handle.done()) { return end(); } } return iterator{handle, false}; } iterator end() { return iterator{nullptr, true}; } // 构造函数、析构函数、移动操作等 explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {} ~Generator() { if (handle) handle.destroy(); } Generator(const Generator&) = delete; Generator& operator=(const Generator&) = delete; Generator(Generator&& other) noexcept : handle(other.handle) { other.handle = nullptr; } Generator& operator=(Generator&& other) noexcept { if (this != &other) { if (handle) handle.destroy(); handle = other.handle; other.handle = nullptr; } return *this; } private: std::coroutine_handle<promise_type> handle; };

实现要点解析:

  1. promise_type:必须定义在Generator内部。get_return_object创建外部控制器。yield_valueco_yield的关键,它接收值并返回一个挂起指示。
  2. final_suspend返回suspend_always:这非常重要!如果返回suspend_never,协程结束时会自动销毁协程帧,可能导致外部迭代器访问悬空内存。挂起后,由Generator析构函数中的handle.destroy()来负责销毁。
  3. 迭代器适配:为了让Generator支持for-range循环,我们实现了begin()end(),以及对应的iteratorbegin()中第一次调用resume()启动协程。
  4. 资源管理:遵循RAII原则,在Generator的析构函数中销毁协程句柄。移动语义的支持也必不可少。

3.2 实现一个简单的异步任务(Task)

Task用于表示一个异步计算,最终会产出一个结果。我们实现一个最简版本:

template<typename T = void> struct Task { struct promise_type { T value; // 存储最终结果 std::exception_ptr exception; // 关键:谁在等待这个任务?这里用一个简单的回调,实际库会用更复杂的调度器 std::coroutine_handle<> continuation; // 等待此任务完成的协程句柄 Task get_return_object() { return Task{std::coroutine_handle<promise_type>::from_promise(*this)}; } // 初始挂起,任务创建后不立即执行,需要手动启动 std::suspend_always initial_suspend() noexcept { return {}; } // 最终挂起,以便在完成后恢复等待它的协程 auto final_suspend() noexcept { struct final_awaiter { bool await_ready() noexcept { return false; } // 总是挂起 // 当任务完成时,恢复等待它的那个协程(如果有的话) std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> h) noexcept { auto& promise = h.promise(); if (promise.continuation) { return promise.continuation; // 返回要恢复的句柄 } return std::noop_coroutine(); // 返回一个空操作句柄,表示结束 } void await_resume() noexcept {} }; return final_awaiter{}; } void return_value(T v) { value = std::move(v); } void unhandled_exception() { exception = std::current_exception(); } }; // 使 Task 本身成为一个 Awaitable bool await_ready() const noexcept { return handle.done(); } // 当另一个协程 co_await 这个 Task 时调用 void await_suspend(std::coroutine_handle<> awaiting_coro) noexcept { // 记录下是哪个协程在等待我 handle.promise().continuation = awaiting_coro; // 启动这个任务(如果还没启动) handle.resume(); } T await_resume() { if (handle.promise().exception) { std::rethrow_exception(handle.promise().exception); } return std::move(handle.promise().value); } // 同步启动任务(仅供最外层非协程环境调用,会阻塞当前线程) T sync_get() { handle.resume(); while (!handle.done()) { // 这是一个简单的演示,实际中这里应该由调度器接管 // 对于无调度器的简单情况,可能陷入死锁,因为任务可能等待其他未启动的任务 // 此处仅为演示,假设任务链是简单的、无循环依赖的。 // 重要:生产环境绝不要这样写! } if (handle.promise().exception) { std::rethrow_exception(handle.promise().exception); } return std::move(handle.promise().value); } std::coroutine_handle<promise_type> handle; // ... 构造函数、析构函数、移动操作类似 Generator ... };

使用示例:

Task<int> async_compute() { co_return 42; } Task<> async_work() { int result = co_await async_compute(); // 等待另一个任务 std::cout << "Got result: " << result << std::endl; } // 在 main 中 int main() { auto task = async_work(); task.handle.resume(); // 手动启动最外层任务 // 需要一个事件循环来驱动所有挂起的协程,直到全部完成。 // 简单演示中,我们假设 async_work 内部没有其他异步等待,所以一次 resume 就完成。 // 实际应用需要调度器(Scheduler)。 }

实现要点解析:

  1. initial_suspend返回suspend_always:这使得Task在创建后是惰性的,不会立即执行。这给了调用者控制权,可以将它提交给调度器。
  2. 链式等待Task通过实现await_ready,await_suspend,await_resume使自己成为一个“可等待体”。这样,一个Task就可以被另一个协程用co_await等待。在await_suspend中,它记录了等待者的句柄(continuation),并在自己完成时(final_suspend中)恢复对方,形成了协程间的等待链。
  3. final_suspend的复杂性:它返回一个自定义的awaiterfinal_awaiter)。这个awaiterawait_suspend方法决定了协程结束后的行为:恢复等待它的协程。这是实现无栈协程链式调度的关键。
  4. 调度器缺失:我们这个简易Task缺少一个核心组件——调度器(Scheduler)。在真实场景中,resume()的调用应该由调度器管理,它可能基于I/O多路复用(如io_uring, epoll)或线程池。协程在co_await一个异步I/O操作时,会将自身注册到调度器并挂起,当I/O就绪时,调度器再调用对应的handle.resume()

重要提醒:手写生产级别的Task和调度器非常复杂,需要考虑线程安全、内存模型、异常安全、性能优化等。通常建议使用成熟的库,如cppcoro或编译器厂商提供的实现。这里的目的是教学,理解其工作原理。

4. 协程实战:构建一个简易的异步HTTP客户端

理解了基础组件,我们来看一个更贴近实际的例子:用协程写一个简单的异步HTTP GET请求。我们将模拟一个异步I/O操作,并使用一个极简的调度器来驱动。

首先,我们定义一个代表异步I/O操作的AsyncIO类型:

struct AsyncReadResult { bool ok; std::string data; int error_code; }; struct AsyncIOAwaiter { int fd; // 模拟的文件描述符 char* buffer; size_t len; AsyncReadResult& result; // 用于存放结果 // 假设有一个全局的调度器可以注册事件 static void register_io_event(int fd, std::coroutine_handle<> h); // 伪代码 bool await_ready() const noexcept { return false; } // 总是假设IO未就绪 void await_suspend(std::coroutine_handle<> h) noexcept { // 将当前协程句柄h和fd注册到调度器(例如epoll) register_io_event(fd, h); // 然后挂起,等待调度器在IO就绪后恢复h } AsyncReadResult await_resume() noexcept { return result; // 恢复时,结果已经由调度器填充好了 } }; // 模拟的异步读函数,返回一个可等待体 AsyncIOAwaiter async_read(int fd, char* buffer, size_t len, AsyncReadResult& result) { return AsyncIOAwaiter{fd, buffer, len, result}; }

接着,我们封装一个基于协程的HTTP GET函数:

Task<std::string> http_get(const std::string& host, const std::string& path) { // 1. 模拟同步DNS解析和连接建立(实际中这也应是异步的) int sockfd = /* socket, connect ... */; // 2. 发送HTTP请求 std::string request = "GET " + path + " HTTP/1.1\r\nHost: " + host + "\r\n\r\n"; send(sockfd, request.data(), request.size(), 0); // 3. 异步读取响应 std::string response; char buf[4096]; AsyncReadResult io_result; while (true) { io_result = co_await async_read(sockfd, buf, sizeof(buf), io_result); if (!io_result.ok || io_result.data.empty()) { break; } response.append(io_result.data.data(), io_result.data.size()); } // 4. 关闭连接 close(sockfd); co_return response; }

最后,我们需要一个主循环(调度器)来驱动所有协程:

// 极简调度器:维护一个就绪队列 std::queue<std::coroutine_handle<>> ready_queue; // 模拟的IO事件注册函数 void register_io_event(int fd, std::coroutine_handle<> h) { // 实际上这里应该将(fd, h)加入到epoll的监听列表中 // 当epoll_wait检测到fd可读时,将对应的h放入ready_queue // 为了演示,我们假设注册后立即模拟一个事件,将h放入队列 ready_queue.push(h); } int main() { // 启动HTTP GET协程(注意:它因为initial_suspend而挂起) auto task = http_get("example.com", "/"); // 手动第一次resume,它会执行到第一个co_await (async_read) 然后挂起 task.handle.resume(); // 事件循环 while (!ready_queue.empty()) { auto h = ready_queue.front(); ready_queue.pop(); h.resume(); // 恢复因IO挂起的协程 // 协程可能执行完,也可能再次遇到co_await挂起 // 如果协程完成,其final_suspend可能会将等待它的其他协程句柄加入ready_queue } // 获取最终结果(需要扩展Task以存储结果) // auto result = task.sync_get(); return 0; }

这个例子揭示了几个关键点:

  1. 协程与异步I/O的绑定co_await等待的是一个代表异步I/O操作的可等待对象。这个对象负责在操作未完成时挂起协程,并将恢复的责任交给事件循环(调度器)。
  2. 调度器的核心作用:调度器(这里是简化的ready_queueregister_io_event)是协程运行时的大脑。它知道哪些协程在等待哪些事件(如文件描述符可读),并在事件就绪时恢复对应的协程。
  3. 代码的线性化:尽管底层是异步的,但http_get函数的代码看起来完全是同步顺序执行的,极大地提升了可读性。

5. 性能、陷阱与最佳实践

协程不是银弹,用不好反而会引入新的复杂度。下面是一些关键的注意事项和实战建议。

5.1 性能考量:栈与堆的博弈

无栈协程(Stackless Coroutines):C++20实现的是无栈协程。挂起时,局部变量存储在堆分配的“协程帧”中,而不是传统的线程栈上。这意味着:

  • 优点:挂起/恢复开销极小,通常只是几个指针操作和条件跳转,比线程上下文切换快几个数量级。可以轻松创建成千上万个协程。
  • 缺点:每次协程调用都可能涉及一次堆内存分配(协程帧)。虽然编译器会努力优化(例如,如果协程生命周期完全在其调用者帧内,可能进行分配消除),但这仍是一个潜在开销点。

优化建议:对于性能极度敏感、生命周期极短的协程,可以考虑使用自定义分配器(通过promise_type::operator new/delete重载)来分配协程帧,例如使用内存池或栈上分配(需谨慎处理生命周期)。

5.2 内存泄漏与生命周期管理

这是协程新手最容易踩的坑。协程帧是手动管理的内存

陷阱1:未销毁已完成的协程

auto make_task() -> Task<> { co_return; } void leak() { auto t = make_task(); // 协程创建,帧在堆上分配 t.handle.resume(); // 协程执行到完成,进入 final_suspend 状态 // 函数结束,t 被销毁,但其析构函数没有调用 handle.destroy() // 协程帧永远泄漏! }

解决:确保协程返回类型(如我们的Task,Generator)的析构函数调用handle.destroy()。同时,promise_type::final_suspend()通常应返回suspend_always或一个自定义的挂起等待器,以阻止协程自动销毁自身。

陷阱2:悬挂引用/指针协程帧中存储着局部变量的引用或指针。如果协程挂起后,这些引用/指针所指向的对象被销毁了,那么恢复协程时将导致未定义行为。

Task<> risky_coro() { std::vector<int> local_vec = {1, 2, 3}; int& ref = local_vec[0]; co_await some_async_op(); // 挂起!此时local_vec仍在帧中,没问题。 // 但如果在挂起期间,有人通过其他方式修改了协程帧?... std::cout << ref; // 恢复后使用ref,安全吗?如果帧被移动了就不安全。 }

解决:尽量避免在协程中持有对不稳定内存的引用。如果必须持有,确保协程生命周期内这些对象绝对有效。对于成员变量,要特别注意对象的生命周期长于协程。

5.3 异常安全

协程中的异常传播与普通函数不同。异常必须通过承诺对象的unhandled_exception()方法捕获并存储(通常存入std::exception_ptr)。当等待该协程的其他协程恢复(await_resume)时,需要检查并重新抛出异常。 我们的Task实现中,await_resume()sync_get()都包含了重新抛出异常的逻辑。这是必须的,否则异常会被静默吞没。

5.4 调试难题

协程的挂起和恢复破坏了传统的函数调用栈。在调试器中,你看到的调用栈可能是不连续的,或者显示为一大堆模板实例化和编译器生成的状态机代码,难以对应到源码行。这对于调试异步逻辑提出了挑战。建议

  • 使用支持协程调试的IDE或工具(如最新版本的Visual Studio、带有特定插件的GDB)。
  • 在关键状态点添加日志输出,记录协程ID和状态。
  • 简化协程逻辑,避免过深的嵌套co_await

5.5 与现有异步库的集成

你很可能已经在使用像libuv,Boost.Asio这样的异步I/O库。好消息是,C++协程可以很好地与它们集成。Boost.Asio从1.70版本就开始提供协程支持(使用boost::asio::awaitable)。其核心是将Asio的异步操作(如async_read)包装成符合Awaitable概念的对象。 集成模式通常是:库提供一个“执行器”(Executor)或“调度器”(Scheduler),你通过co_spawn或类似函数将协程提交给它执行,库负责在I/O事件就绪时恢复对应的协程。

6. 协程的适用场景与不适用场景

经过前面的深入剖析,你应该对C++协程的“能”与“不能”有了更清晰的认识。我们来做个总结。

非常适合使用协程的场景:

  1. 高并发I/O密集型服务:这是协程的主场。网络服务器(HTTP/API服务器、游戏服务器、代理服务器)、数据库中间件等,需要同时处理成千上万个连接,每个连接上的操作大部分时间都在等待网络I/O。使用“一个连接一个协程”的模型,代码清晰度远超基于回调的状态机,而性能与基于事件循环的异步模型相当,且资源占用远低于“一个连接一个线程”。
  2. 生成器与惰性求值:需要按需生成或消费序列的场景,如解析大型文件/流、遍历复杂数据结构、实现数学序列(如斐波那契数列)、自定义范围适配器(C++20 Ranges的协程支持)。Generator模式在这里无可替代。
  3. 状态机实现:那些原本需要用显式状态变量(如enum State)和switch-case实现的复杂状态机,用协程来实现会异常优雅。每个状态可以写成一段连续的代码,通过co_awaitco_yield进行状态转移。
  4. 协作式多任务:在游戏开发、UI框架或嵌入式系统中,需要一种轻量级的、由任务自身主动让出CPU的多任务机制。协程是比线程更合适的选择。

不建议或需谨慎使用协程的场景:

  1. 纯计算密集型任务:如果你的任务几乎全是CPU计算,没有I/O等待,那么协程带来的收益微乎其微。线程(或许结合线程池)仍然是利用多核的首选。协程的切换开销虽小,但频繁切换无等待的任务反而会增加开销。
  2. 对延迟有极端要求的硬实时系统:协程的调度(由你或你的调度器控制)是非抢占式的。如果一个协程陷入死循环或长时间计算而不挂起,它会阻塞整个线程。在硬实时系统中,需要可预测的、严格的时间保证,通常使用抢占式RTOS和精心设计的线程优先级。
  3. 极度简单的异步逻辑:如果异步操作只有一两个层级,使用std::future.then续接或许更简单。引入协程需要定义相关类型,有点“杀鸡用牛刀”。
  4. 团队技术栈不匹配:协程是C++20的新特性,需要编译器支持(MSVC 2019+, GCC 10+, Clang 11+)。如果团队或项目环境无法升级,则无法使用。此外,团队需要时间学习协程的思维模式和调试技巧。

7. 迈向下一步:生态、工具与学习资源

掌握了基础,想要在项目中应用,你还需要了解以下内容。

现有协程库:

  • cppcoro:Lewis Baker 开发的一个著名的协程库,提供了丰富的 awaitable 类型(如task,generator,async_mutex,async_auto_reset_event)以及基于IOCP的调度器(Windows)。它是学习高级协程用法的绝佳资源。
  • Boost.Asio:如前所述,Asio提供了协程支持,是构建跨平台网络应用的强大选择。
  • 编译器厂商的实现:MSVC的STL实现中包含了一些协程工具,如std::generator(C++23)。libc++和libstdc++也在逐步跟进。

调试工具与技巧:

  • Visual Studio:对C++协程的调试支持较好,可以查看协程状态和局部变量。
  • GDB/LLDB:需要一些技巧。可以打印std::coroutine_handle的地址,但解读内部状态较难。添加调试打印是最朴实有效的方法。
  • 日志注入:在承诺对象的initial_suspend,final_suspend,yield_value,return_value等关键点注入日志,输出协程ID和关键参数,是追踪复杂协程流程的利器。

学习路径建议:

  1. 理解概念:吃透本文所述的核心机制(三巨头、承诺对象、句柄)。
  2. 动手实现:跟着第3节实现一遍简化的GeneratorTask,这是加深理解最有效的方式。
  3. 阅读优秀代码:仔细阅读cppcoro库的源码,尤其是task.hppgenerator.hpp,看看工业级实现如何处理异常、调度和内存。
  4. 集成现有库:尝试用Boost.Asio的协程写一个小型TCP回声服务器。
  5. 关注演进:关注C++26标准提案中关于协程的改进,如std::lazy,以及错误处理方面的提案。

C++协程是一把锋利的瑞士军刀,它改变了我们处理异步和惰性计算的方式。初学时觉得晦涩是正常的,因为它引入了一套全新的编译期抽象。但一旦你理解了其状态机的本质和“挂起-恢复”的协作式哲学,就会发现它带来的代码清晰度提升是革命性的。从今天开始,尝试在你的下一个I/O密集型模块中,用协程替换掉那些令人头疼的回调吧。