C++ 中的协程:从概念到实践

协程(Coroutines)是 C++20 标准中正式加入的一个强大特性,它为异步编程提供了一种更直观、更高效的方式。相比传统的基于回调或线程池的异步实现,协程能让代码保持同步的写法,同时避免了“回调地狱”和上下文切换的开销。本文将从协程的基本概念、关键关键词、典型实现以及实际应用几个角度,帮助读者快速掌握协程的核心要点。

1. 协程的基本概念

  • 挂起(Suspend):协程在执行过程中可以主动挂起,让出控制权。
  • 恢复(Resume):挂起后的协程可以被外部或内部恢复继续执行。
  • 状态机:协程内部的执行流被编译器转换为状态机,保持挂起点的上下文。

协程与线程不同,它们是轻量级的任务单元,切换的成本几乎为零。协程的“挂起点”由 co_await, co_yield, co_return 三个关键字来标记。

2. 关键关键词解读

关键词 作用 典型用法
co_await 挂起协程,等待一个可等待对象(Awaitable)完成 int value = co_await async_fetch();
co_yield 暂停协程并返回一个值给调用者,类似生成器 co_yield i;
co_return 结束协程,返回最终值 co_return result;

可等待对象(Awaitable)必须实现 await_ready(), await_suspend(), await_resume() 三个成员函数。标准库提供了如 std::future, std::generator, std::task 等实现,也可以自定义。

3. 协程的实现细节

3.1 编译器生成的状态机

编译器会把协程函数拆分为若干块,每个块对应一个 co_await, co_yield, co_return 的位置。状态机内部维护一个 promise_type 对象,保存协程的局部变量、异常信息和返回值。协程入口 operator() 会先调用 promise_type::get_return_object() 获取协程句柄,然后直接执行到第一个挂起点。

3.2 协程句柄(std::coroutine_handle

句柄是协程的运行时入口,提供 resume(), destroy(), done() 等成员函数。通过句柄可以手动控制协程的执行。

std::coroutine_handle <promise_type> h = coro();  // 启动协程
while (!h.done()) h.resume();  // 逐步恢复
h.destroy();  // 释放资源

在异步框架中,句柄通常与事件循环(Event Loop)结合使用,按需恢复协程。

4. 常见协程模型

模型 说明 示例
协程+事件循环 事件循环驱动协程的恢复,适合 I/O 密集型 asio::co_spawn
协程+线程池 线程池负责执行耗时操作,协程负责协作 std::asyncco_await
协程+生成器 通过 co_yield 实现惰性序列 `std::generator
seq()`

5. 实际案例:异步文件读取

下面给出一个完整示例,演示如何使用 co_await + asio(Boost.Asio 1.75+ 或 standalone ASIO)实现异步文件读取。

#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/detached.hpp>
#include <fstream>
#include <iostream>

using namespace boost::asio;
using awaitable = awaitable<void, io_context::executor_type>;

awaitable read_file(const std::string& path) {
    // 打开文件
    std::ifstream file(path, std::ios::binary | std::ios::ate);
    if (!file) {
        co_return;  // 文件不存在
    }

    // 获取文件大小
    std::size_t size = static_cast<std::size_t>(file.tellg());
    file.seekg(0, std::ios::beg);

    // 读取内容
    std::vector <char> buffer(size);
    co_await async_read(
        /* handler */, 
        buffer, 
        use_awaitable);
    std::cout << "Read " << buffer.size() << " bytes.\n";
    co_return;
}

int main() {
    io_context io;
    co_spawn(io, read_file("example.txt"), detached);
    io.run();
}

注意:示例中使用了 use_awaitable 来将传统异步 API 转化为 Awaitable 对象,简化了协程与 I/O 的耦合。

6. 性能对比

场景 回调 线程池 + std::async 协程
线程切换 1 次/任务 1 次/任务 0 次/任务
代码可读性
资源占用 线程栈 线程栈 协程栈 ~ 几 KB
错误传播 通过回调传递 异常跨线程 异常直接抛出

从表格可以看出,协程在 I/O 密集型任务中能显著降低上下文切换成本,同时保持代码的同步结构。

7. 进阶话题

  • 协程与 RAII:协程内部资源的生命周期管理要靠 promise_type 或者自定义析构逻辑。
  • 协程池:类似线程池,协程池可预分配协程句柄,降低频繁创建的开销。
  • 与现有框架的结合:如 cppcoro, folly::coro, QtQFuture 等。

8. 小结

  • 协程是 C++20 引入的轻量级异步机制,使用 co_await, co_yield, co_return 控制挂起与恢复。
  • 关键在于实现 Awaitable 对象和事件循环。
  • 与传统回调相比,协程具有更好的可读性和更低的运行时成本。

掌握协程后,你可以将异步 I/O、网络通信、并发计算等场景写得更简洁、更高效。未来的 C++ 程序员,协程已成为不可或缺的技能之一。

发表评论