**C++20 中的协程(Coroutines)究竟是什么?**

C++20 在标准库中正式引入了协程(Coroutines)这一强大的语言特性,旨在简化异步编程、生成器以及延迟计算等模式。与传统的线程、回调或手写状态机相比,协程提供了一种更直观、更高效、更易维护的写法。下面从定义、实现原理、典型使用场景以及常见坑等方面进行系统剖析。


一、协程的基本概念

  1. 协程(Coroutine)是一种可暂停、可恢复的函数。它允许在函数内部“挂起”执行,稍后再恢复,而不必返回到调用者的栈帧。与线程不同,协程在同一线程中切换,避免了线程上下文切换的成本。

  2. C++ 的协程使用关键字 co_await, co_yield, co_return 等来声明挂起点,并且通过 promiseawaitable 两个核心概念来连接协程与外部的调度器。

  3. 协程本质上是一个状态机,编译器会把含有挂起点的函数编译为一个生成器类,该类拥有内部状态、栈帧和继续点信息。


二、协程实现的工作流程

  1. 定义协程函数

    std::future <int> asyncAdd(int a, int b) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        co_return a + b;          // 立即返回,协程结束
    }
  2. 协程的调用
    调用协程返回一个 awaitable 对象(如 std::future 或自定义 generator)。该对象不立即执行函数,而是保存协程入口点。

  3. 等待协程
    调用者通过 co_await.get() 等方式,触发协程的实际执行。执行过程中遇到 co_await 会挂起,直到 awaitable 完成;遇到 co_yield 会返回一个值给调用者,并暂停。

  4. 调度器
    对于 std::future 等标准实现,调度器是基于线程池的。自定义协程往往需要实现自己的调度器,用于把协程挂起点关联到事件循环(如 asioio_context)或自定义任务队列。


三、典型使用场景

场景 传统实现 协程实现 优点
异步 I/O 回调 + 状态机 co_await + asio::awaitable 代码更像同步,错误处理更自然
生成器 手写迭代器 co_yield 简洁、可读性高
协同多任务 线程 + 互斥 协程 + 事件循环 低延迟、低资源占用
延迟计算 函数返回 std::function co_return 更易组合、链式调用

四、常见陷阱与最佳实践

  1. 不当的 awaitable

    • 问题:在协程中 co_await 一个永不完成的 awaitable 会导致程序挂起。
    • 解决:确保所有 awaitable 有明确的完成路径,或使用超时/取消机制。
  2. 内存泄漏

    • 问题:协程内部的对象可能被延迟析构,导致资源在协程挂起期间未释放。
    • 解决:使用 RAII,确保所有资源在协程作用域内及时析构;或者显式在 finally 块中释放。
  3. 异常传播

    • 问题:异常在协程内部抛出后,会被封装进 std::futureexception_ptr,调用者需要显式获取。
    • 解决:在调用 co_await.get() 前使用 try/catch,并注意异常是否已经被捕获。
  4. 性能开销

    • 问题:协程生成的状态机类体积较大,频繁创建会产生 GC 与分配开销。
    • 解决:复用协程对象(如使用 generator 对象池),或将协程定义为单次使用。
  5. 调试困难

    • 问题:协程内部暂停点难以定位。
    • 解决:使用支持协程调试的 IDE(如 CLion 2023.1+),或在关键挂起点插入日志。

五、实战示例:基于 asio 的协程 HTTP 客户端

#include <boost/asio.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/awaitable.hpp>
#include <iostream>

using namespace boost::asio;
using tcp = ip::tcp;

awaitable<std::string> fetch(const std::string& host, const std::string& path) {
    auto executor = co_await this_coro::executor;
    tcp::resolver resolver(executor);
    auto const results = co_await resolver.async_resolve(host, "http", use_awaitable);
    tcp::socket socket(executor);
    co_await async_connect(socket, results, use_awaitable);

    // 发送 HTTP GET 请求
    std::string req = "GET " + path + " HTTP/1.1\r\nHost: " + host + "\r\nConnection: close\r\n\r\n";
    co_await async_write(socket, buffer(req), use_awaitable);

    // 接收响应
    std::string resp;
    boost::system::error_code ec;
    while (true) {
        std::array<char, 512> buf;
        std::size_t n = co_await async_read(socket, buffer(buf), use_awaitable, ec);
        if (ec == error::eof) break;
        resp.append(buf.data(), n);
    }
    co_return resp;
}

int main() {
    io_context ctx;
    co_spawn(ctx, []() -> awaitable <void> {
        auto body = co_await fetch("example.com", "/");
        std::cout << body.substr(0, 200) << "...\n";
    }, detached);
    ctx.run();
}

此示例展示了如何用协程完成一个完整的异步 HTTP GET 请求。与传统回调方式相比,代码更简洁、可读。


六、总结

C++20 的协程为异步编程提供了语言级别的原生支持,使得之前需要手写状态机、回调或使用第三方库的场景可以用更直观、更高效的方式实现。虽然协程在引入时会带来一定学习成本,但掌握后能够显著提升代码质量与运行性能。建议从生成器或简单的协程任务入手,逐步引入复杂的异步 I/O 框架,形成自己的协程编程习惯。

发表评论