C++20中的协程:从概念到实践

协程(Coroutines)是C++20中一个重要的新特性,它为编写异步、非阻塞代码提供了更直观、更高效的方式。与传统的回调、Future或线程模型相比,协程通过让函数在执行过程中“挂起”和“恢复”,实现了类似同步代码的可读性,却不需要额外的线程开销。本文将从协程的基本概念、实现原理、标准库支持以及实际应用场景展开详细介绍,并给出完整的代码示例。

1. 协程的基本概念

1.1 什么是协程?

协程是一种在同一线程中多点挂起与恢复的函数。它可以在需要的时候“暂停”执行,保存当前状态,然后在后续再恢复执行。相比普通函数,协程可以在多处返回,而不必一次性返回完整结果。

1.2 与线程的区别

  • 线程:每个线程都有独立的栈和调度,创建线程开销大。
  • 协程:共享同一线程,栈由协程内部实现管理,开销极低。
  • 协程适用于I/O密集型、事件驱动等场景;线程适用于CPU密集型并行计算。

2. C++20协程的实现原理

2.1 关键语法元素

  • co_await:等待一个异步操作完成。
  • co_yield:产生一个值并挂起协程。
  • co_return:返回协程最终结果并结束协程。

2.2 生成器(Generator)模式

使用 co_yield 可以轻松实现一个生成器。编译器会生成一个内部状态机,管理协程的生命周期和局部变量的保存。

2.3 协程句柄(std::coroutine_handle

协程句柄用于手动控制协程的挂起与恢复。标准库提供了 std::suspend_alwaysstd::suspend_never 作为默认的挂起策略。

3. 标准库对协程的支持

模块 功能 说明
std::generator(C++23) 生成器 通过 co_yield 生成一系列值
std::task(C++23) 异步任务 co_await 为核心
std::future 异步结果 与协程结合,支持 co_await
std::suspend_always / suspend_never 挂起策略 控制挂起与立即继续

注意:截至 C++20,生成器等高级功能仅在 C++23 标准中正式加入,C++20 中可使用第三方库(如 cppcoro、boost::asio 等)或手写协程包装器。

4. 实际应用示例

下面以一个简单的异步文件读取为例,演示如何使用协程配合 std::future 实现非阻塞 I/O。代码使用 C++20 标准,假设操作系统提供了异步文件读取 API(如 io_uringReadFileEx)。

#include <iostream>
#include <coroutine>
#include <future>
#include <cstring>
#include <filesystem>
#include <fstream>

namespace fs = std::filesystem;

// 简单的异步文件读取模拟
struct AsyncReadResult {
    std::string data;
    std::exception_ptr eptr = nullptr;
};

struct AsyncFileReader {
    struct promise_type {
        AsyncReadResult result;

        AsyncFileReader get_return_object() {
            return AsyncFileReader{ std::coroutine_handle <promise_type>::from_promise(*this) };
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_value(AsyncReadResult r) { result = std::move(r); }
        void unhandled_exception() { result.eptr = std::current_exception(); }
    };

    std::coroutine_handle <promise_type> handle;

    AsyncFileReader(std::coroutine_handle <promise_type> h) : handle(h) {}
    ~AsyncFileReader() { if (handle) handle.destroy(); }

    // 用作 co_await 的 awaiter
    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> awaiting) noexcept {
        // 模拟异步 I/O: 这里直接使用同步读取并在后台线程完成
        std::thread([this, awaiting](){
            try {
                std::ifstream file;
                file.open(handle.promise().result.data); // 数据字段暂时用于文件名
                std::stringstream buffer;
                buffer << file.rdbuf();
                handle.promise().result.data = buffer.str();
            } catch (...) {
                handle.promise().result.eptr = std::current_exception();
            }
            awaiting.resume(); // 恢复调用者
        }).detach();
    }
    AsyncReadResult await_resume() {
        if (handle.promise().eptr) std::rethrow_exception(handle.promise().eptr);
        return std::move(handle.promise().result);
    }
};

// 协程函数,读取文件内容
AsyncFileReader read_file_async(const std::string& path) {
    AsyncReadResult res;
    res.data = path; // 暂存文件名
    co_return res;
}

// 主程序
int main() {
    try {
        std::cout << "开始异步读取文件...\n";
        auto reader = read_file_async("sample.txt");
        auto result = co_await reader; // 等待协程完成
        std::cout << "文件内容长度: " << result.data.size() << " 字节\n";
    } catch (const std::exception& e) {
        std::cerr << "读取失败: " << e.what() << '\n';
    }
}

关键点说明

  1. AsyncFileReader:包装了协程句柄,并实现了 await_ready/suspend/resume 逻辑。
  2. read_file_async:协程函数,返回 AsyncFileReader,可以被 co_await
  3. 后台线程:在 await_suspend 内部启动异步读取,完成后恢复协程。
  4. 错误处理:通过 unhandled_exception 捕获异常,await_resume 再次抛出。

在实际项目中,可将上述逻辑与系统底层的异步 I/O API(如 libuvboost::asioio_uring)结合,进一步提升性能。

5. 小结

C++20 的协程特性为现代 C++ 开发提供了更直观、轻量级的异步编程模型。通过掌握 co_awaitco_yieldco_return 等关键字,以及标准库的协程支持,开发者可以在保持代码可读性的同时,减少线程切换开销,提升程序的并发性能。未来的 C++23 将进一步完善协程生态,建议关注标准化进展,并尝试在项目中逐步迁移到协程式编程。

发表评论