**C++20 的协程:用法与实践**

在 C++20 标准中,协程(Coroutines)被正式纳入语言规范,为异步编程提供了更直观、性能更优的解决方案。与传统的回调、线程或手动状态机相比,协程让代码更接近同步写法,同时保持非阻塞和高效。本文将从协程的基本概念、关键语法到实战案例,逐步展开讲解。


一、协程的核心概念

  1. 暂停与恢复
    协程通过 co_awaitco_yieldco_return 关键字在执行过程中“挂起”与“恢复”,使得函数可以在多个点暂停执行,随后从上一次挂起的位置继续。

  2. 生成器(Generator)与任务(Task)

    • Generator:使用 co_yield 产生一系列值,类似于 Python 的生成器。
    • Task:使用 co_return 返回最终结果,类似于 Future/Promise。
  3. 协程的句柄(Coroutine Handle)
    每个协程都有一个句柄 std::coroutine_handle,它可用来检查协程是否已完成、主动恢复或销毁协程。


二、关键语法与实现细节

关键字 作用 典型示例
co_await 等待一个 awaitable 对象,协程挂起直到其完成 int value = co_await asyncRead();
co_yield 生成一个值,暂停协程,让调用者获取该值 co_yield i;
co_return 返回最终值,结束协程 co_return result;
co_await 前的 co_ 前缀 标识协程操作,编译器会生成相应的 state machine co_await std::suspend_always{};

Awaitable 类型
任何具备以下成员的类型都可以作为 awaitable:

struct Awaitable {
    bool await_ready() const noexcept;
    void await_suspend(std::coroutine_handle<> h) noexcept;
    T await_resume() noexcept;
};
  • await_ready():判断是否需要挂起。
  • await_suspend():挂起协程,传入当前句柄。
  • await_resume():协程恢复时返回的值。

三、实战示例:异步文件读取

下面给出一个完整的协程实现,用来异步读取文件内容,并返回字符串。示例使用了 std::filesystemstd::future 作为后端异步机制。

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

struct AsyncReadResult {
    std::string data;
    bool success;
};

struct AsyncReadAwaitable {
    std::string filename;
    std::promise <AsyncReadResult> promise;

    bool await_ready() const noexcept { return false; }

    void await_suspend(std::coroutine_handle<> h) {
        std::thread([this, h]() {
            std::ifstream in(filename, std::ios::binary);
            AsyncReadResult res;
            if (in) {
                res.data.assign((std::istreambuf_iterator <char>(in)),
                                std::istreambuf_iterator <char>());
                res.success = true;
            } else {
                res.success = false;
            }
            promise.set_value(res);
            h.resume(); // 这里不需要显式恢复,因为我们使用了 asyncFuture 的机制
        }).detach();
    }

    AsyncReadResult await_resume() { return promise.get_future().get(); }
};

struct AsyncReadTask {
    struct promise_type {
        std::coroutine_handle <promise_type> get_return_object() {
            return std::coroutine_handle <promise_type>::from_promise(*this);
        }
        std::suspend_never initial_suspend() noexcept { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void unhandled_exception() { std::terminate(); }
        void return_value(AsyncReadResult value) { result = value; }
        AsyncReadResult result;
    };

    std::coroutine_handle <promise_type> handle;
    AsyncReadResult result;

    AsyncReadTask(std::coroutine_handle <promise_type> h) : handle(h), result(h.promise().result) {}

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

AsyncReadTask asyncReadFile(const std::string& filename) {
    AsyncReadResult res = co_await AsyncReadAwaitable{filename};
    co_return res;
}

int main() {
    auto task = asyncReadFile("example.txt");
    if (task.result.success) {
        std::cout << "文件内容:" << task.result.data << std::endl;
    } else {
        std::cout << "读取失败" << std::endl;
    }
    return 0;
}

说明

  1. AsyncReadAwaitable:包装文件读取逻辑,内部用 std::thread 异步执行。
  2. asyncReadFile:使用协程实现的异步读取函数,返回 AsyncReadTask
  3. AsyncReadTask:协程句柄包装器,提供 result 成员供调用者直接访问。

四、性能与注意事项

  1. 协程本身无开销
    协程在挂起/恢复时不涉及栈复制或线程切换,只是更新内部状态机。真正的开销来自于 awaitable 对象所做的异步操作。

  2. 不要滥用
    过度使用协程会导致句柄数目暴增,尤其在高并发网络服务器中,需要对协程池或调度器做细粒度管理。

  3. 异常安全
    协程在异常抛出时会走 unhandled_exception(),默认调用 std::terminate。如果需要捕获异常,可在 promise_type::return_voidreturn_value 里进行处理。


五、进一步阅读

  • 《C++20 标准草案》相关章节
  • 《Effective Modern C++》中关于协程的讨论
  • 官方实现库如 cppcoroasio::awaitable

通过上述介绍,你应该能对 C++20 协程有一个系统的了解,并在实际项目中快速尝试异步编程。协程将成为 C++ 未来异步开发的核心工具,值得每位 C++ 开发者深入掌握。

发表评论