C++20协程:从理论到实践

C++20 协程是 C++ 标准库和语言层面的一大创新,为异步编程提供了一种更为直观、内聚的实现方式。它既兼容传统的回调、Future/Promise 方案,也为更复杂的控制流(如生成器、协程管道)提供了天然支持。本文将系统地介绍协程的概念、实现细节,并通过一个完整的异步 I/O 示例展示如何在实际项目中使用。

1. 协程的基本概念

在 C++20 之前,异步操作往往通过回调函数或多线程的方式实现。协程的核心思想是“暂停”和“恢复”函数的执行,形成一个可以在不同时间点切换的执行单元。协程的运行机制可以用以下几个关键词来概括:

  • co_await:等待一个异步操作完成,并将执行挂起。
  • co_yield:在生成器中返回一个值,但保留执行状态以供后续恢复。
  • co_return:结束协程,返回最终结果。

协程本身是一个特殊的函数,返回类型必须是 std::futurestd::generator 或用户自定义的“协程类型”。编译器在编译时会把协程拆分成若干个小状态机,隐藏在内部实现细节。

2. 协程的实现细节

2.1 协程句柄(std::coroutine_handle

协程句柄是协程内部状态机的入口,拥有:

template<class Promise>
class coroutine_handle {
public:
    void resume();          // 继续执行
    bool done() const;      // 检查是否完成
    Promise& promise();     // 访问协程 Promise 对象
};

编译器会为每个协程生成一个对应的 promise_type,其中存放协程的局部状态和返回值。

2.2 Promise 对象

promise_type 用于在协程暂停时保存状态,或在协程结束时提供返回值。典型的 promise_type 需要实现:

struct promise_type {
    std::coroutine_handle <promise_type> get_return_object();
    std::suspend_never initial_suspend();   // 初始挂起策略
    std::suspend_always final_suspend();    // 结束挂起策略
    void return_value(T value);             // 传递返回值
    void unhandled_exception();             // 异常处理
};

通过自定义 promise_type,我们可以控制协程何时挂起、返回什么类型的对象以及异常如何传播。

2.3 Suspend / Yield 策略

  • std::suspend_always:每次调用都会挂起,适合需要显式挂起的地方。
  • std::suspend_never:永不挂起,适合不需要暂停的地方。

在协程中,co_await 会根据被等待对象的 await_ready()await_suspend()await_resume() 三个成员函数来决定是否挂起。

3. 一个完整的异步 I/O 示例

下面演示如何使用协程实现一个简易的异步文件读取器,利用 C++20 的标准库和 Boost.Asio。

3.1 准备工作

sudo apt-get install libboost-all-dev

3.2 代码实现

// async_file_reader.cpp
#include <iostream>
#include <fstream>
#include <string>
#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/thread_pool.hpp>

using namespace boost::asio;
using namespace std::chrono_literals;

namespace asio = boost::asio;
using boost::asio::awaitable;
using boost::asio::use_awaitable;

// 读取文件内容的协程
awaitable<std::string> read_file(const std::string& path) {
    // 使用一个异步线程池
    thread_pool pool(1);
    // 打开文件
    co_await pool.async_spawn([path](awaitable <void> self) -> awaitable<void> {
        std::ifstream ifs(path, std::ios::binary);
        if (!ifs) {
            throw std::runtime_error("无法打开文件");
        }
        // 读取文件内容
        std::string content((std::istreambuf_iterator <char>(ifs)), std::istreambuf_iterator<char>());
        // 通过 co_return 返回
        co_return content;
    }, use_awaitable);
}

// 主协程入口
awaitable <void> main_co() {
    try {
        std::string data = co_await read_file("sample.txt");
        std::cout << "文件内容长度: " << data.size() << " 字节\n";
        std::cout << "前 100 字符: " << data.substr(0, 100) << "\n";
    } catch (const std::exception& e) {
        std::cerr << "异常: " << e.what() << "\n";
    }
}

int main() {
    // 运行协程
    asio::io_context io;
    asio::co_spawn(io, main_co(), asio::detached);
    io.run();
    return 0;
}

3.3 运行与效果

g++ -std=c++20 -pthread async_file_reader.cpp -lboost_system -lboost_thread -o async_reader
./async_reader

程序会异步读取 sample.txt 并打印前 100 个字符,展示了协程如何在后台线程中执行 I/O 而不阻塞主线程。

4. 协程与传统异步方案的对比

特性 传统回调 std::future / Promise 协程
可读性
错误传播 手动 自动 自动
代码复用
性能 依赖线程/事件循环 轻量 极轻量(仅状态机)

协程最大的优势在于让异步代码看起来像同步代码,避免了“回调地狱”或“Future 链式调用”导致的代码混乱。

5. 常见陷阱与注意事项

  1. 生命周期管理:协程对象和其内部 promise_type 的生命周期紧密绑定,必须确保协程句柄不被提前销毁。
  2. 异常安全:在协程中抛出的异常会被封装进 future,使用 co_await 时需捕获。
  3. 资源释放:如果协程在异步操作中占用了资源(如文件句柄),应在 final_suspendco_return 处显式释放,防止泄漏。
  4. 线程安全:协程本身不保证线程安全,若在多线程中共享协程对象,需要自行同步。

6. 结语

C++20 协程为语言带来了更强的异步表达能力,让复杂的异步控制流变得简单易读。无论是网络编程、文件 I/O,还是游戏循环,协程都能提供更为直观的实现方式。随着库生态(如 Boost.Asio、cppcoro 等)的完善,协程正逐渐成为现代 C++ 开发的核心技术之一。希望本文能帮助你快速上手并将协程融入实际项目。

发表评论