C++20 中的协程(Coroutines):实用示例与注意事项

协程(Coroutine)是 C++20 引入的一项强大特性,允许函数在执行过程中暂停并在以后恢复,从而实现异步编程、生成器以及更直观的流控制。相比传统的回调或 Future,协程在语义上更接近同步代码,降低了复杂度。本文将从概念入手,介绍协程的基本构造、典型使用场景、关键 API 以及常见坑,配合完整示例帮助你快速上手。


1. 协程基本概念

关键词 解释
co_await 在协程内部等待一个 awaitable 对象(如 std::future、自定义 Awaitable 等)
co_yield 生成一个值,类似生成器中的 yield
co_return 结束协程并返回结果
co_await std::suspend_always / std::suspend_never 明确指定协程何时挂起或不挂起

协程并非单独线程,而是一种轻量级的状态机。编译器会将带有协程关键字的函数展开为一个状态机类,内部维护悬挂点和返回值。


2. 标准协程接口

2.1 std::suspend_alwaysstd::suspend_never

struct suspend_always {
    bool await_ready()  const noexcept { return false; }
    void await_suspend(std::coroutine_handle<>) const noexcept {}
    void await_resume()  const noexcept {}
};

struct suspend_never {
    bool await_ready()  const noexcept { return true; }
    void await_suspend(std::coroutine_handle<>) const noexcept {}
    void await_resume()  const noexcept {}
};
  • suspend_always:协程始终挂起,等待外部显式 resume。
  • suspend_never:协程不挂起,直接执行完毕。

2.2 std::coroutine_handle

template<class Promise>
struct coroutine_handle {
    static coroutine_handle from_promise(Promise& promise);
    void resume();          // 恢复执行
    bool done() const;      // 是否已结束
    void destroy();         // 释放资源
};

开发者通常不直接使用 coroutine_handle,除非需要自定义协程调度器。


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

下面演示如何用协程实现异步读取文件,读取完毕后返回字符串。

#include <coroutine>
#include <string>
#include <iostream>
#include <fstream>
#include <filesystem>

struct AsyncReadResult {
    std::string content;
    bool success = false;
};

struct AsyncRead {
    struct promise_type {
        AsyncReadResult result;
        std::coroutine_handle <promise_type> next{};

        AsyncRead get_return_object() {
            return AsyncRead{std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept {
            // 这里可以让协程自动完成后执行下一步
            if (next) next.resume();
            return {};
        }
        void return_value(AsyncReadResult val) { result = val; }
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle <promise_type> handle;

    AsyncRead(std::coroutine_handle <promise_type> h) : handle(h) {}
    AsyncRead(const AsyncRead&) = delete;
    AsyncRead& operator=(const AsyncRead&) = delete;

    ~AsyncRead() {
        if (handle) handle.destroy();
    }

    void resume() { if (!handle.done()) handle.resume(); }
};

AsyncRead read_file_async(const std::string& path) {
    co_await std::suspend_always{}; // 模拟异步挂起

    AsyncReadResult res;
    try {
        std::ifstream in(path, std::ios::binary);
        if (!in) throw std::runtime_error("打开文件失败");
        std::string data((std::istreambuf_iterator <char>(in)),
                         std::istreambuf_iterator <char>());
        res.content = std::move(data);
        res.success = true;
    } catch (...) {
        res.success = false;
    }

    co_return res;
}

int main() {
    auto task = read_file_async("example.txt");

    // 手动调度协程
    task.resume(); // 第一次 resume 会进入协程主体
    // 由于我们在协程中使用了 suspend_always,第二次 resume 继续执行
    task.resume();

    auto& result = task.handle.promise().result;
    if (result.success) {
        std::cout << "读取内容:" << result.content.substr(0, 100) << "...\n";
    } else {
        std::cout << "读取失败\n";
    }
}

说明

  1. AsyncRead 封装了协程句柄与 promise,简化使用。
  2. 通过 co_await std::suspend_always{} 模拟异步挂起,实际项目可替换为真正的 I/O 完成事件。
  3. final_suspend 用于在协程结束后自动恢复下一步操作,演示协程链式调用的便利。

4. 协程的常见陷阱

陷阱 解决办法
忘记 co_return 必须在协程尾部使用 co_return 或抛异常,避免隐式返回导致未定义行为。
promise 对象被提前销毁 保证协程句柄存活至协程结束,避免在外部捕获 std::coroutine_handle 时误删。
异常传播 promise_type 中实现 unhandled_exception,将异常包装或记录,避免程序崩溃。
多线程协程 协程本身并非线程安全,若在多线程中共享协程对象,需要使用互斥或专用调度器。
资源泄漏 始终在 final_suspend~AsyncRead 中释放句柄和 promise;不要忘记调用 destroy()

5. 协程与传统异步框架对比

特性 协程 std::future / std::async
代码风格 同步写法,易读 回调或链式 Future,代码可读性下降
性能 轻量级,堆栈共享 线程/线程池开销
错误处理 直接抛异常 需要检查状态
调度 可自定义调度器 受线程池限制

如果项目中需要频繁进行 I/O、网络请求或需要实现生成器,协程无疑是更优选。


6. 小结

  • C++20 协程通过 co_await / co_yield / co_return 简化异步编程。
  • 标准库提供 std::suspend_always / suspend_nevercoroutine_handle 等工具。
  • 示例演示了异步文件读取的完整实现。
  • 注意异常传播、资源释放和线程安全,避免常见坑。

掌握协程后,你将能够编写更简洁、更高效的异步代码,提升项目的可维护性与性能。祝编码愉快!

发表评论