C++20协程:从概念到实践

在C++20之前,异步编程常常需要使用回调、状态机或第三方库(如Boost.Asio)来实现。随着C++20对协程(coroutine)的官方支持,编写异步、懒加载和流式处理代码变得更加直观、可维护。本文从协程的核心概念、语法要点到一个完整的文件读取协程示例,带你快速上手。


1. 协程的核心概念

  1. 挂起点(yield)与恢复点(resume)

    • 协程在 co_await, co_yield, co_return 处暂停执行。
    • 通过 handle.resume() 重新激活协程。
  2. 协程句柄(coroutine_handle)

    • 表示协程的运行时控制对象,负责管理协程的生命周期。
  3. 悬空与完成

    • handle.done() 判断协程是否已完成。
    • 需要显式 handle.destroy() 以释放资源。
  4. 返回类型

    • 协程函数的返回类型是 `std::future ` 或自定义类型,内部实现会使用 `promise_type`。

2. 语法要点

关键字 用途 示例
co_await 等待一个异步操作完成,挂起协程 auto result = co_await asyncRead();
co_yield 从协程返回一个值,挂起当前协程 co_yield value;
co_return 返回最终结果并结束协程 co_return finalResult;
co_return + `std::optional
| 用于可返回值或无值的协程 |co_return std::nullopt;`

自定义返回类型

template<typename T>
struct Generator {
    struct promise_type;
    using handle_type = std::coroutine_handle <promise_type>;

    struct promise_type {
        T current_value;
        std::suspend_always yield_value(T value) { current_value = value; return {}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        Generator get_return_object() { return {handle_type::from_promise(*this)}; }
        void return_void() {}
        void unhandled_exception() { std::exit(1); }
    };

    handle_type coro;
    explicit Generator(handle_type h) : coro(h) {}
    ~Generator() { if (coro) coro.destroy(); }
    T next() {
        coro.resume();
        return coro.promise().current_value;
    }
};

3. 实际案例:懒加载文件读取

以下示例演示如何使用协程逐行读取大文件,避免一次性加载到内存。

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

struct LineGenerator {
    struct promise_type {
        std::string current;
        std::ifstream file;

        LineGenerator get_return_object() {
            return LineGenerator{std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        std::suspend_always yield_value(std::string&& line) {
            current = std::move(line);
            return {};
        }

        void return_void() {}
        void unhandled_exception() { std::exit(1); }
    };

    using handle_type = std::coroutine_handle <promise_type>;
    handle_type coro;

    explicit LineGenerator(handle_type h) : coro(h) {}
    ~LineGenerator() { if (coro) coro.destroy(); }

    // 取得下一行
    bool next(std::string& out) {
        if (!coro.done()) {
            coro.resume();
            if (coro.done()) return false;
            out = std::move(coro.promise().current);
            return true;
        }
        return false;
    }
};

LineGenerator read_lines(const std::string& path) {
    std::ifstream in(path);
    std::string line;
    while (std::getline(in, line)) {
        co_yield std::move(line);
    }
}

int main() {
    std::string line;
    for (auto gen = read_lines("large.txt"); gen.next(line); ) {
        std::cout << line << '\n';   // 逐行处理
    }
    return 0;
}

关键点解析

  • read_lines 是协程函数,返回 LineGenerator
  • 每次 co_yield 时,协程挂起,保存当前行。
  • 调用者通过 gen.next(line) 触发 resume,读取下一行。

4. 常见陷阱与建议

  1. 忘记 handle.destroy()
    • 协程句柄默认不销毁,必须手动释放,避免内存泄漏。
  2. 错误的 initial_suspend/final_suspend
    • 选择 suspend_always 可以在调用时直接挂起,适合懒加载。
  3. 异常处理
    • promise_type::unhandled_exception 默认调用 std::terminate,可根据需要自定义。
  4. 多线程协程
    • 协程本身不是线程安全的,需在同一线程中调用 resume
  5. std::future 混用
    • 若想与线程池配合,可让 co_await 调用 std::async 返回的 std::future

5. 结语

C++20 协程为异步编程提供了更接近同步代码的语义,使得复杂的 IO、网络、状态机逻辑可以用更简洁、可读的方式实现。掌握协程的基本语法、返回类型设计与协程句柄的管理,是编写高性能、可维护 C++20 代码的关键。祝你在项目中愉快地探索协程的魅力!

发表评论