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

在 C++20 中协程(coroutines)正式被纳入标准库,带来了异步编程的新范式。本文从协程的基本概念出发,逐步深入实现细节,最终给出一个完整的协程示例,帮助读者快速上手并掌握协程的核心技术点。


1. 什么是协程?

协程是一种轻量级的函数,能够在执行过程中暂停(co_awaitco_yieldco_return)并在以后恢复执行。它们与传统的线程相比,拥有更低的上下文切换成本,且可以在单线程环境下实现异步 IO、数据流处理等功能。

1.1 协程的三大关键字

关键字 用途 示例
co_await 等待一个可等待对象(awaitable),并在其完成后恢复协程 auto result = co_await async_operation();
co_yield 产生一个值,挂起协程,直到下一个值被请求 co_yield i;
co_return 结束协程并返回最终结果 co_return final_result;

1.2 协程与线程的区别

维度 协程 线程
调度 由协程库或运行时决定 由操作系统调度
上下文切换 仅保存程序计数器、栈指针等少量状态 完整的 CPU 状态(寄存器、栈等)
资源占用 栈空间可按需分配 需要完整栈空间
并发方式 单线程异步 多线程并行

2. 协程的实现机制

C++ 协程的实现并非直接在语言层面完成,而是通过编译器把协程函数展开成一个状态机。编译器会生成一个结构体(通常叫做“悬挂结构”)来保存协程的内部状态,包括:

  • Promise 类型:定义协程的返回值类型、异常处理等。
  • 悬挂句柄:`std::coroutine_handle `,用于手动控制协程的生命周期。
  • 协程入口resume(),让协程从上一次挂起的位置继续执行。

2.1 Promise 类型

promise_type 定义了协程的“宿主”,其成员函数决定协程如何处理返回值、异常、以及挂起/恢复行为。例如:

struct my_promise {
    int value_;                      // 存储最终返回值

    my_promise() = default;
    ~my_promise() = default;

    // 必须提供一个 get_return_object(),返回一个能被调用者使用的对象
    std::coroutine_handle <my_promise> get_return_object() noexcept {
        return std::coroutine_handle <my_promise>::from_promise(*this);
    }

    // 用于生成协程入口点的初始 suspend
    std::suspend_always initial_suspend() noexcept { return {}; }

    // 用于协程结束时的最终 suspend
    std::suspend_always final_suspend() noexcept { return {}; }

    // 设置最终返回值
    void return_value(int v) noexcept { value_ = v; }

    // 处理异常
    void unhandled_exception() {
        std::terminate();
    }
};

2.2 协程函数展开

假设有一个协程函数:

std::future <int> async_add(int a, int b) {
    co_return a + b;
}

编译器会把它展开成类似以下的代码(简化版):

struct async_add_promise {
    // 与 my_promise 同理
};

async_add_promise async_add_impl(int a, int b) {
    async_add_promise p;
    // 计算结果
    p.return_value(a + b);
    return p;
}

std::future <int> async_add(int a, int b) {
    auto handle = std::coroutine_handle <async_add_promise>::from_promise(async_add_impl(a, b));
    handle.resume();              // 立即执行到第一次挂起点
    // 这里会得到一个 future 对象,供调用方异步等待
    return std::future <int>{ /* ... */ };
}

3. 一个完整的协程示例

下面我们实现一个“异步文件读取”协程。假设我们要从磁盘读取一个文件内容,并在读取完成后返回字符串。我们将使用 co_await 结合 std::experimental::awaitable(在 std::execution/std::ranges 中提供)。

注意:实际代码中需要依赖一个可等待的异步 IO 库,如 Boost.Asio 或 C++标准实验性协程库。此处为演示简化实现。

3.1 Awaitable 类型

#include <coroutine>
#include <string>
#include <iostream>

template<typename T>
class awaitable {
public:
    struct promise_type;
    using handle_type = std::coroutine_handle <promise_type>;

    awaitable(handle_type h) : coro_(h) {}
    awaitable(const awaitable&) = delete;
    awaitable& operator=(const awaitable&) = delete;
    awaitable(awaitable&& o) noexcept : coro_(o.coro_) { o.coro_ = nullptr; }
    ~awaitable() { if (coro_) coro_.destroy(); }

    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> awaiting) {
        // 这里可以把 awaiter 关联到真正的 IO 操作
        // 简化起见,直接恢复协程
        coro_.resume();
    }
    T await_resume() { return coro_.promise().value_; }

private:
    handle_type coro_;
};

template<typename T>
struct awaitable <T>::promise_type {
    T value_;
    std::suspend_always initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void return_value(T v) noexcept { value_ = v; }
    void unhandled_exception() { std::terminate(); }
    awaitable get_return_object() noexcept {
        return awaitable{std::coroutine_handle <promise_type>::from_promise(*this)};
    }
};

3.2 异步文件读取协程

#include <fstream>
#include <filesystem>
#include <sstream>

awaitable<std::string> async_read_file(const std::string& path) {
    std::ifstream ifs(path, std::ios::binary);
    if (!ifs) {
        co_return std::string(); // 读取失败返回空字符串
    }
    std::stringstream buffer;
    buffer << ifs.rdbuf();
    co_return buffer.str();
}

3.3 主程序调用

int main() {
    auto read_task = async_read_file("example.txt");
    // 这里我们简单地同步等待协程完成
    std::string content = read_task.await_resume(); // 直接获取结果
    std::cout << "文件内容长度: " << content.size() << std::endl;
    return 0;
}

在实际项目中,你会使用事件循环或线程池来异步等待协程完成,而不是直接调用 await_resume()。以上代码仅展示协程的基本使用方式。


4. 常见协程陷阱与调试技巧

陷阱 解决方案
Promise 对象被销毁 确保协程句柄在使用完毕前保持有效,或使用 std::unique_ptr 等智能指针管理
异常未捕获 promise_typeunhandled_exception 中加入日志或抛出自定义异常
悬挂句柄泄漏 使用 std::coroutine_handle::destroy()std::unique_ptr 自动释放
无效的 co_await 对象 确认 awaitable 满足 await_readyawait_suspendawait_resume 三个接口

调试技巧

  1. 使用编译器诊断:GCC/Clang 支持 -Wcooperative-Wcoro 等警告,帮助发现协程错误。
  2. 手动输出协程状态:在 await_suspendawait_resume 中打印日志,追踪协程挂起/恢复的时间点。
  3. 单步调试:IDE(如 CLion、Visual Studio)可以在 co_await 处停下,查看悬挂句柄与 Promise 状态。

5. 结语

C++20 的协程为异步编程带来了前所未有的便利。通过学习协程的基本概念、实现机制和实战示例,开发者可以在保持代码可读性与可维护性的同时,构建高性能的异步应用。建议在实际项目中逐步引入协程,先从单一 IO 操作开始,慢慢扩展到更复杂的任务调度、流水线处理等高级场景。祝编码愉快!

发表评论