**C++20 中的协程:从基础到实战**

C++20 新增的协程(coroutine)特性,为异步编程提供了更简洁、更高效的语法。相比传统的回调或 promise‑future 方式,协程以同步的写法实现非阻塞逻辑,极大提升代码可读性。本文将从协程的基本概念、关键字使用,到实际案例——实现一个异步数据流消费,逐步带你掌握协程的核心。

一、协程的基本概念

协程是“可以暂停和恢复执行的函数”。在 C++ 中,协程由 co_awaitco_yieldco_return 三个关键字实现,并且需要一个 协程返回类型(如 std::futurestd::generator 或自定义类型)。协程在执行到 co_await 时会挂起,等待外部事件完成后恢复执行;co_yield 用于生成值流,调用方通过迭代获取每个生成值。

二、关键字解析

关键字 用途 说明
co_await 暂停协程等待某个可等待对象完成 只适用于返回类型为 std::futurestd::task
co_yield 产生一个值,暂停协程 常用于实现生成器(generator)
co_return 结束协程并返回结果 生成最终返回值或结束信号

三、协程返回类型

C++20 并未提供统一的协程返回类型,而是留给实现者自定义。最常见的两种:

  1. **`std::future `**:标准库提供的异步结果容器。协程可以返回 `std::future`,调用方通过 `get()` 获取结果。
  2. **`std::generator `**(或自定义 `generator`):用于生成值流,类似 Python 的 `yield`。

备注:std::generator 在标准库中并未直接定义,但在很多实现(如 libstdc++、libc++)中可用。若不想依赖实现,可自定义一个简单的生成器。

四、实战示例:异步文件读取 + 数据流消费

下面演示一个完整的协程应用:读取一个文本文件,逐行解析后生成关键字列表,并异步处理每个关键字。

1. 工具类:异步文件读取

#include <fstream>
#include <string>
#include <future>

struct async_file_reader {
    std::ifstream in;
    async_file_reader(const std::string& path) : in(path) {}
    // 返回一个 future,表示一次读取行操作
    std::future<std::string> read_line() {
        return std::async(std::launch::async, [this] {
            std::string line;
            if (std::getline(in, line))
                return line;
            else
                return std::string(); // 空串表示 EOF
        });
    }
};

2. 协程生成器:逐行读取

#include <coroutine>
#include <exception>

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

    struct promise_type {
        T current_value;
        std::exception_ptr exc;

        generator get_return_object() {
            return generator{handle_type::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(T value) {
            current_value = value;
            return {};
        }
        void return_void() {}
        void unhandled_exception() { exc = std::current_exception(); }
    };

    handle_type coro;
    generator(handle_type h) : coro(h) {}
    ~generator() { if (coro) coro.destroy(); }
    generator(const generator&) = delete;
    generator& operator=(const generator&) = delete;
    generator(generator&& other) noexcept : coro(other.coro) { other.coro = nullptr; }
    generator& operator=(generator&& other) noexcept {
        if (this != &other) {
            if (coro) coro.destroy();
            coro = other.coro;
            other.coro = nullptr;
        }
        return *this;
    }

    class iterator {
        handle_type coro;
    public:
        explicit iterator(handle_type h) : coro(h) {
            if (coro) coro.resume();
        }
        iterator& operator++() {
            coro.resume();
            return *this;
        }
        const T& operator*() const { return coro.promise().current_value; }
        const T* operator->() const { return &coro.promise().current_value; }
        bool operator==(std::default_sentinel_t) const { return !coro || coro.done(); }
    };

    iterator begin() { return iterator{coro}; }
    std::default_sentinel_t end() { return {}; }
};

3. 协程实现:读取并生成关键字

generator<std::string> line_generator(const std::string& path) {
    async_file_reader reader(path);
    while (true) {
        auto fut = reader.read_line();
        std::string line = fut.get(); // 这里为了示例同步等待,可改为 co_await
        if (line.empty()) break;
        co_yield line; // 暂停协程并返回当前行
    }
}

注意:在真实异步场景中,应使用 co_await fut 而非 fut.get(),以避免阻塞线程。

4. 消费器:并行处理关键字

void process_keywords(const std::string& file_path) {
    for (auto&& line : line_generator(file_path)) {
        // 简单示例:将每行拆分为单词
        std::istringstream iss(line);
        std::string word;
        while (iss >> word) {
            // 这里模拟异步处理(例如网络请求)
            std::async(std::launch::async, [word] {
                std::cout << "Processing keyword: " << word << std::endl;
            });
        }
    }
}

5. 主函数

int main() {
    const std::string file = "sample.txt";
    process_keywords(file);
    // 给异步任务留足时间完成(真实项目应更优雅的同步)
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 0;
}

五、性能与注意事项

  • 协程内存占用:协程需要维护状态机,编译器会生成堆栈帧。避免在协程中大对象直接存放,可使用 co_yield std::move(obj) 或 `co_yield std::make_shared ()`。
  • 异常传播:协程的 promise_type 可捕获异常,co_return 会抛出异常给调用方。
  • 调试难度:协程在调试时可能导致栈跟踪失真,建议在调试时使用 -g 并结合 lldbgdb 的协程支持。

六、结语

C++20 的协程为异步编程提供了极具表达力的工具。通过理解 co_awaitco_yield 与自定义协程返回类型,你可以在保持同步代码可读性的同时实现高性能的异步逻辑。未来的标准(C++23、C++26)预计会进一步完善协程生态,加入更丰富的可等待对象、标准库容器支持等。保持关注,掌握协程,你的 C++ 项目将更具竞争力。

发表评论