C++20 中协程(Coroutines)的实用案例

协程是 C++20 引入的一个强大特性,它通过语言层面的支持,让异步编程变得既简洁又直观。下面通过一个具体的“懒加载文件行读取”示例,来展示协程在日常编程中的实用价值。

1. 什么是协程?

协程(Coroutine)是一种可以暂停与恢复执行的函数。它不再像传统函数那样一次性完成全部工作,而是在需要时挂起(yield),随后可以在同一状态下继续执行。C++20 通过 co_awaitco_yieldco_return 关键字,以及 std::experimental::coroutine(在标准库中被移至 std::coroutine)实现协程。

2. 目标需求

我们想实现一个懒加载的文件行读取器:

  • 懒加载:文件只有在真正需要读取下一行时才去读取,避免一次性把整个文件读入内存。
  • 可迭代:可以像普通范围一样使用 for 循环遍历。

3. 关键技术点

  1. generator 模板:一个简单的协程返回类型,包装协程句柄、生成器值。
  2. co_yield:用于把每一行返回给调用者,同时挂起协程。
  3. co_return:在文件末尾结束协程。

4. 代码实现

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

// 1. 简单的 generator 实现
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 exception;

        auto get_return_object() {
            return generator{handle_type::from_promise(*this)};
        }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        std::suspend_always yield_value(T value) {
            current_value = value;
            return {};
        }
        void return_void() noexcept {}

        void unhandled_exception() {
            exception = std::current_exception();
        }
    };

    handle_type coro;

    explicit 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;
    }

    // Iterator 结构
    struct iterator {
        handle_type coro;
        bool done;

        explicit iterator(handle_type h, bool d) : coro(h), done(d) {}

        iterator& operator++() {
            coro.resume();
            done = coro.done();
            return *this;
        }
        T operator*() const { return coro.promise().current_value; }
        bool operator==(const iterator& other) const { return done == other.done; }
        bool operator!=(const iterator& other) const { return !(*this == other); }
    };

    iterator begin() {
        if (coro) {
            coro.resume();
            if (coro.done()) return iterator{coro, true};
            return iterator{coro, false};
        }
        return iterator{coro, true};
    }
    iterator end() { return iterator{coro, true}; }
};

// 2. 协程函数:懒加载文件行
generator<std::string> lazy_read_lines(const std::string& path) {
    std::ifstream fin(path);
    if (!fin.is_open()) {
        throw std::runtime_error("Cannot open file: " + path);
    }

    std::string line;
    while (std::getline(fin, line)) {
        co_yield line;          // 暂停协程,返回当前行
    }
    // 文件读取完毕,协程结束
}

// 3. 使用示例
int main() {
    try {
        auto lines = lazy_read_lines("example.txt");
        for (const auto& line : lines) {
            std::cout << line << '\n';
        }
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << '\n';
    }
    return 0;
}

5. 代码解读

  • generator:封装了协程句柄和 iterator,提供 begin()/end() 使其能与范围 for 配合。
  • lazy_read_lines:打开文件后循环读取每行,使用 co_yield 将行返回给调用者。因为 co_yield 之后协程挂起,文件读取操作保持懒惰。
  • main:直接遍历 generator 对象即可像处理普通容器一样读取文件行。

6. 性能与优势

  1. 内存占用:只保持当前行,适用于大文件。
  2. 代码简洁:省去了手动维护状态机或迭代器类。
  3. 可组合性:协程可以与 std::async、网络 I/O 等异步操作无缝结合。

7. 进一步拓展

  • 错误处理:在协程内部捕获异常并通过 promise_type::unhandled_exception 传播。
  • 多线程:在协程内部使用 co_await std::suspend_until 等实现并发读取。
  • 标准库支持:C++23 已将 std::generator 定义在标准库中,代码可进一步简化。

8. 小结

C++20 的协程为我们提供了一种既强大又优雅的异步编程方式。通过上述懒加载文件行读取的例子,展示了协程在实际项目中的实用价值。掌握好协程的语法与实现细节,可以让你的 C++ 代码在性能和可维护性上都有显著提升。

发表评论