C++20 协程(Coroutines)在异步编程中的应用

在 C++20 标准中,协程(coroutines)被正式纳入语言层面,为编写异步代码提供了更直观、类型安全且高性能的方案。与传统的回调、promise/future 机制相比,协程可以让你像编写顺序代码一样写出非阻塞逻辑,极大降低代码的复杂度与错误率。

1. 协程基本概念

协程是一种“可挂起”的函数。通过 co_awaitco_yieldco_return 等关键字,协程可以在执行过程中暂停并保存状态,随后在合适时机恢复执行。核心特性:

  • 协程类型:`std::coroutine_handle ` 用来管理协程的生命周期。
  • 悬挂点co_awaitco_yieldco_return 都是悬挂点,决定协程暂停的位置。
  • 悬挂对象co_await 后面跟随的对象需要满足 Awaitable 协议(实现 await_readyawait_suspendawait_resume)。

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

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

struct AsyncFileReader {
    struct promise_type;
    using handle_type = std::coroutine_handle <promise_type>;

    struct promise_type {
        std::string buffer;
        std::string file_path;
        std::string& get_return_object() { return *this; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { std::terminate(); }
        void return_void() {}
    };

    AsyncFileReader(handle_type h) : handle(h) {}
    ~AsyncFileReader() { if (handle) handle.destroy(); }

    handle_type handle;
};

AsyncFileReader readFileAsync(std::string path) {
    std::ifstream file(path, std::ios::binary);
    if (!file) throw std::runtime_error("Cannot open file");

    std::string content((std::istreambuf_iterator <char>(file)),
                        std::istreambuf_iterator <char>());

    co_await std::suspend_always{}; // 模拟异步暂停
    co_return;
}

int main() {
    auto reader = readFileAsync("example.txt");
    // 在这里可以继续执行其他任务
    reader.handle.resume(); // 恢复协程
    std::cout << "文件已读取完成" << std::endl;
}

上面代码仅作演示,实际使用时建议结合线程池或事件循环来真正实现异步 I/O。

3. 与标准库协程适配器

C++20 标准库提供了 std::futurestd::async 与协程的桥接方式。常见模式:

  • std::future + co_awaitco_await std::async([]{ return heavy_compute(); });
  • 自定义 awaitable:实现 await_readyawait_suspendawait_resume,可直接在协程中使用 co_await

4. 性能与错误处理

  • 无栈拷贝:协程生成器不必在栈上复制所有局部变量,减少内存占用。
  • 异常传递:协程通过 promise_type::unhandled_exception() 把异常提升到协程外层,可与 try/catch 配合使用。
  • 调试难度:协程的挂起与恢复可能导致堆栈帧不连续,调试时需使用支持 C++20 的调试器。

5. 进阶:自定义事件循环

在高性能服务器或游戏引擎中,常见做法是:

  1. 事件循环:循环调用 handle.resume(),当协程 co_await 一个等待对象时会挂起。
  2. 等待对象:如 TimerAwaitableSocketAwaitable,在 await_suspend 中将协程注册到对应事件源。
  3. 事件回调:当事件到来后,调度器将对应协程恢复执行。

示例伪代码:

struct TimerAwaitable {
    std::chrono::steady_clock::time_point when;
    std::coroutine_handle<> awaiting;
    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> h) noexcept {
        awaiting = h;
        scheduler.schedule(when, h); // 注册到事件循环
    }
    void await_resume() noexcept {}
};

awaitable sleep_for(std::chrono::milliseconds ms) {
    return TimerAwaitable{std::chrono::steady_clock::now() + ms};
}

6. 结语

C++20 的协程为编写高并发、低延迟的程序提供了强大工具。通过正确设计 awaitable、事件循环与错误处理机制,开发者可以在保持代码可读性的同时,获得接近系统底层性能的异步体验。随着编译器与标准库的不断完善,协程将在未来的 C++ 生态中扮演越来越核心的角色。

发表评论