C++20 协程(Coroutines)的使用与实践

协程(Coroutines)是 C++20 标准中一项重要的新特性,它为异步编程、协作式多任务提供了一种更简洁、可读性更高的语法。与传统的回调或 Promise 方式相比,协程可以让代码像同步那样书写,却在底层实现了异步执行。本文将从协程的基本概念、关键语法、实现细节以及常见应用场景等方面进行阐述,并给出完整的代码示例,帮助读者快速上手。

1. 协程的核心概念

  • 协程函数:标记为 co_awaitco_yieldco_return 的函数。它可以在执行过程中挂起(suspend)并在需要时恢复。
  • promise_type:每个协程函数都关联一个 promise_type,用于管理协程的生命周期、返回值、异常以及挂起点。
  • std::coroutine_handle:指向协程状态的句柄,通过它可以控制协程的执行(resume、destroy 等)。
  • 悬挂点co_awaitco_yieldco_return 等关键字出现的位置称为悬挂点,决定了协程何时挂起。

2. 关键语法

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

2.1 协程函数返回类型

C++20 允许协程返回一个拥有 promise_type 的类型,最常见的是 std::futurestd::generator 等。我们可以自定义一个简单的 Task 类型来演示:

struct Task {
    struct promise_type {
        int value_;
        Task get_return_object() { return Task{std::coroutine_handle <promise_type>::from_promise(*this)}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { std::terminate(); }
        void return_value(int v) { value_ = v; }
    };

    std::coroutine_handle <promise_type> h_;
    int get() { return h_.promise().value_; }
    ~Task() { if (h_) h_.destroy(); }
};

2.2 关键字使用

  • co_await expr:等待 expr 的结果,挂起当前协程。
  • co_yield expr:在生成器中产生一个值,挂起当前协程。
  • co_return expr:结束协程并返回 expr

3. 示例:异步文件读取

下面演示一个简化的异步文件读取协程。实际项目中可以结合 std::filesystemasioboost::asio 等 IO 库。

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

struct AsyncReadResult {
    std::vector <char> buffer;
};

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

    std::coroutine_handle <promise_type> h_;
    AsyncReadResult get() { return std::move(h_.promise().result_); }
    ~AsyncReadTask() { if (h_) h_.destroy(); }
};

AsyncReadTask async_read_file(const std::string& path) {
    std::ifstream file(path, std::ios::binary | std::ios::ate);
    if (!file) {
        throw std::runtime_error("文件打开失败");
    }

    std::streamsize size = file.tellg();
    file.seekg(0, std::ios::beg);
    std::vector <char> buffer(static_cast<size_t>(size));
    if (!file.read(buffer.data(), size)) {
        throw std::runtime_error("读取失败");
    }

    // 模拟异步挂起
    co_await std::suspend_always{};

    AsyncReadResult res{std::move(buffer)};
    co_return std::move(res);
}

int main() {
    try {
        AsyncReadTask task = async_read_file("example.txt");
        // 在这里可以执行其他工作
        AsyncReadResult result = task.get();
        std::cout << "文件大小: " << result.buffer.size() << " 字节\n";
    } catch (const std::exception& ex) {
        std::cerr << "异常: " << ex.what() << std::endl;
    }
    return 0;
}

3.1 说明

  • async_read_file 通过 co_await std::suspend_always 模拟一次挂起,实际异步场景会在 IO 完成后再恢复。
  • AsyncReadTaskpromise_typereturn_value 用来传递读取结果。

4. 常见 pitfalls 与调试建议

  1. 忘记 co_return:协程没有返回值时,co_return 可写作 co_return;。若遗漏,编译器会报错。
  2. 异常泄露:如果协程内部抛出异常,promise_type::unhandled_exception 需要妥善处理,否则会导致程序终止。
  3. 资源泄露std::coroutine_handle 需要手动销毁,建议使用 RAII 包装。
  4. 性能开销:协程在内部会创建堆上对象(如 promise),过度使用会产生 GC 噪音。适当使用 std::suspend_alwaysstd::suspend_never 控制挂起点。

5. 典型应用场景

场景 说明
异步网络 IO asiolibuv 等事件循环结合,实现高并发网络服务器。
并行计算 使用 co_await 配合 std::asyncstd::thread,实现任务拆分与协作。
生成器 通过 co_yield 生成无限序列(如斐波那契数列)。
GUI 事件驱动 在 UI 主线程与后台线程之间同步数据,避免阻塞。

6. 进一步阅读

  1. C++20 标准草案 – 章节 29.7 “协程”。
  2. 《C++协程实战》 – 详细介绍协程的设计与应用。
  3. 官方库 cppcoro – 提供高层次协程封装。

结语

协程为 C++ 提供了统一、强大的异步编程模型,降低了回调地狱与 Promise 链式调用的复杂度。掌握基本语法、了解 promise 机制并结合实际项目场景进行练习,是快速成为协程高手的关键。祝你编码愉快!

发表评论