C++20协程:让异步编程变得简单

在现代 C++ 开发中,异步编程与并发已经成为不可或缺的技术。传统上,开发者需要依赖第三方库(如 Boost.Asio、libuv)或自己实现事件循环和状态机,代码往往冗长且易出错。C++20 引入了 协程(Coroutines),为异步编程提供了语言级别的支持,既能保持高性能,又能让代码更易读、更接近同步写法。

1. 协程的基本概念

协程是一种轻量级的用户级线程,它可以在执行过程中被挂起(co_awaitco_yieldco_return),然后在需要时恢复。与线程不同,协程在挂起时不会阻塞底层线程,只是将执行状态保存到堆上,等到恢复时再继续执行。

C++20 协程需要满足以下三条规则:

  1. co_await:在协程中等待一个 awaitable 对象。协程会挂起,直到 awaitable 变为就绪状态。
  2. co_yield:在协程中产生一个值,并将执行挂起,等待下一个 co_await 或者外部的消费。
  3. co_return:终止协程并返回最终值。

协程的返回类型通常是一个 std::experimental::generator 或自定义的 Awaitable 类型,或者直接使用 std::future 等。

2. 简单示例:异步读取文件

假设我们要在后台异步读取文件内容,并在主线程中处理结果。使用协程,可以这样实现:

#include <iostream>
#include <fstream>
#include <string>
#include <future>
#include <experimental/coroutine>

struct AsyncReadResult {
    std::string content;
    bool error;
};

struct AsyncReadAwaitable {
    std::string filename;
    std::experimental::coroutine_handle<> handle;
    AsyncReadResult result;

    bool await_ready() const noexcept { return false; } // 总是挂起
    void await_suspend(std::experimental::coroutine_handle<> h) {
        handle = h;
        std::thread([=]() mutable {
            std::ifstream in(filename, std::ios::binary);
            if (in) {
                result.content.assign((std::istreambuf_iterator <char>(in)),
                                      std::istreambuf_iterator <char>());
                result.error = false;
            } else {
                result.error = true;
            }
            handle.resume(); // 恢复协程
        }).detach();
    }
    AsyncReadResult await_resume() { return result; }
};

AsyncReadAwaitable async_read(const std::string& filename) {
    return AsyncReadAwaitable{filename};
}

struct AsyncReadTask {
    struct promise_type {
        AsyncReadTask get_return_object() { return {}; }
        std::experimental::suspend_never initial_suspend() { return {}; }
        std::experimental::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};

AsyncReadTask read_file(const std::string& fname, std::function<void(const AsyncReadResult&)> callback) {
    co_await async_read(fname);
    callback({/*content from await_resume*/});
}

上述代码中,async_read 返回一个 Awaitable,内部使用 std::thread 进行文件 I/O,然后通过 handle.resume() 恢复协程。主程序可以调用 read_file 并提供回调,真正实现异步读取。

3. 使用协程生成器

C++20 还提供了 std::generator,可以用来创建值流,类似于 Python 的生成器。下面是一个产生斐波那契数列的示例:

#include <iostream>
#include <experimental/generator>

std::experimental::generator <int> fib(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield a;
        int tmp = a + b;
        a = b;
        b = tmp;
    }
}

int main() {
    for (int v : fib(10)) {
        std::cout << v << " ";
    }
    std::cout << std::endl;
}

运行结果为 0 1 1 2 3 5 8 13 21 34。协程让生成器的实现非常简洁,只需 co_yield 即可。

4. 与 async / await 语法的对比

虽然 C++20 协程语法与 C# 或 JavaScript 的 async/await 有相似之处,但它们并不是完全等价:

  • 协程是无状态的:C++ 的协程在挂起时将整个执行上下文保存,恢复时不需要显式的状态机。
  • 底层实现:C++ 协程可以与同步代码无缝集成,也可以结合 std::futurestd::promise 等实现更高级的异步任务。
  • 性能:协程在 C++ 中的实现更接近底层,往往比使用线程池+消息队列的方案更轻量。

5. 小结

C++20 的协程为异步编程提供了强大的工具,让异步代码更易读、维护成本更低。它不仅支持传统的 I/O 协程,还能用于生成器、事件循环等场景。随着编译器对协程的优化,未来的 C++ 项目将更加高效且易于开发。希望本篇文章能帮助你快速上手 C++20 协程,并在实际项目中得到应用。

发表评论