C++20 中的协程:实用指南

协程(Coroutines)是 C++20 标准中引入的一项重要特性,旨在简化异步编程、生成器和事件驱动模型的实现。相比传统的线程、回调或 Promise,协程提供了更直观、更可维护的代码结构。本文将从协程的基本概念、实现细节、使用场景以及性能考虑等方面进行系统梳理,帮助读者快速掌握并应用协程技术。

1. 协程的基本概念

在 C++ 之前,异步编程往往需要使用回调、线程或第三方库(如 Boost.Asio、std::future 等)。这些方案的缺点是代码层次分散、错误易错、难以组合。协程通过在函数内部挂起(yield)与恢复(resume)的方式,将程序的执行流程拆分为多个“段”,使得异步操作看似同步。

核心术语:

  • 协程函数:使用 co_awaitco_yieldco_return 的函数,返回值类型为 std::futurestd::generatorstd::task 等。
  • 挂起点co_awaitco_yieldco_return 所在位置,函数会在此处挂起。
  • 状态机:编译器将协程函数转换为状态机对象,负责保存局部变量与执行点。

2. 典型实现方式

C++20 标准提供了三种协程返回类型,分别适用于不同的使用场景。

2.1 std::generator(生成器)

#include <coroutine>
#include <iostream>
#include <optional>

template<typename T>
struct generator {
    struct promise_type {
        std::optional <T> current;
        generator get_return_object() { return generator{std::coroutine_handle <promise_type>::from_promise(*this)}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(T value) { current = std::move(value); return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

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

    bool next() { return coro.resume(), !coro.done(); }
    T value() { return std::move(*coro.promise().current); }
};

generator <int> fibonacci(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield a;
        std::tie(a, b) = std::make_pair(b, a + b);
    }
}

使用方式:

for (auto val : fibonacci(10)) {
    std::cout << val << ' ';
}

2.2 std::future(异步任务)

C++20 引入了 co_awaitstd::future 的集成。下面演示一个简单的异步计算:

#include <future>
#include <chrono>

std::future <int> async_add(int a, int b) {
    co_return a + b; // 自动包装为 std::future <int>
}

int main() {
    auto fut = async_add(5, 7);
    std::cout << "Result: " << fut.get() << '\n';
}

若想与事件循环结合,可使用 co_await 对已完成的 std::future

std::future <int> delayed(int ms, int value) {
    std::this_thread::sleep_for(std::chrono::milliseconds(ms));
    co_return value;
}

2.3 自定义 Task(适用于事件循环)

如果你想在自己的事件循环中调度协程,最好自定义一个 Task 类型并实现 await_transform。以下为简化示例:

#include <coroutine>
#include <iostream>
#include <queue>
#include <chrono>
#include <thread>

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

Task simple_task(int id) {
    std::cout << "Task " << id << " start\n";
    co_return;
}

在事件循环中:

std::queue<std::function<void()>> loop;
loop.push([]{ std::cout << "Hello from loop\n"; });

while (!loop.empty()) {
    auto job = std::move(loop.front());
    loop.pop();
    job();
}

3. 使用场景

  1. 异步 I/O
    std::asyncboost::asio 结合,可让 I/O 代码像同步那样写。co_await 在等待 I/O 时挂起,释放线程资源。

  2. 生成器
    用于遍历大数据集、文件行、网络数据包等。生成器不需要一次性把所有数据加载到内存。

  3. 协程管道
    多个协程串联形成数据流(类似 Go 的 channel),可实现流式处理、数据清洗等。

  4. 游戏循环
    任务调度器 + 协程可以实现分帧、状态机、动画等功能。

4. 性能与注意事项

  • 内存占用:协程对象会保存局部变量,若局部变量较大,建议使用 co_yieldco_await 把数据传递给外部,而不是复制。
  • 异常传播:协程内部抛出的异常会通过 promise_type::unhandled_exception 处理,若未处理会调用 std::terminate。可以在 promise 中自定义 unhandled_exception
  • 上下文切换:协程切换相较于线程切换更轻量,但仍需避免频繁挂起/恢复。建议在协程内部执行的同步工作尽量快。

5. 示例:基于协程的 HTTP 客户端

下面给出一个使用 libcurl 的异步 HTTP 请求示例(伪代码,实际需要自行实现 CurlAwaitable):

#include <coroutine>
#include <curl/curl.h>
#include <iostream>

struct CurlAwaitable {
    CURL* easy;
    std::string buffer;
    CURLcode result;

    CurlAwaitable(CURL* e) : easy(e) {}

    bool await_ready() { return false; }
    void await_suspend(std::coroutine_handle<> h) {
        // 设置写回调
        curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, [](char* ptr, size_t size, size_t nmemb, void* userdata) {
            auto& buf = *static_cast<std::string*>(userdata);
            buf.append(ptr, size * nmemb);
            return size * nmemb;
        });
        curl_easy_setopt(easy, CURLOPT_WRITEDATA, &buffer);
        // 异步执行
        curl_easy_perform(easy);
        h.resume();
    }
    std::string await_resume() { return buffer; }
};

std::future<std::string> async_http_get(const std::string& url) {
    CURL* easy = curl_easy_init();
    curl_easy_setopt(easy, CURLOPT_URL, url.c_str());
    co_return co_await CurlAwaitable(easy);
}

int main() {
    auto fut = async_http_get("https://api.github.com");
    std::cout << fut.get() << std::endl;
}

6. 结语

C++20 的协程为异步编程提供了更接近同步代码的写法,降低了错误率、提升了可读性。掌握协程的基本语法、返回类型与事件循环框架,将使你在处理 I/O、生成器、流式处理等任务时更加得心应手。建议从小型项目实验起,逐步引入协程到生产代码中,以便充分了解其优势与局限。祝你编码愉快!

发表评论