C++20 中的协程:如何在现代项目中实现异步编程

协程(Coroutines)是 C++20 标准中引入的一项强大功能,旨在简化异步编程和并发代码的书写。与传统的回调或线程模型相比,协程提供了更接近同步代码的可读性,同时保持了高效的执行性能。本文将从协程的基本概念、关键语法、实现原理以及实际使用场景四个方面展开讨论,并给出一段完整的示例代码,帮助你快速上手。

1. 协程的基本概念

协程是一种特殊的函数,其执行可以在任意点被挂起(yield)并在稍后恢复。协程的执行上下文(包括局部变量、寄存器状态、栈帧等)会被保存,以便后续继续执行。协程本质上是一种轻量级的用户级线程,调度由程序员或库负责,而非操作系统内核。

在 C++20 中,协程通过以下三个核心概念实现:

  • co_await:表示等待一个可等待对象,协程会挂起,直到等待对象完成。
  • co_yield:从协程返回一个值,并将协程挂起,等待下一次请求。
  • co_return:终止协程并返回最终值。

2. 关键语法与实现细节

2.1 协程返回类型

协程函数必须返回一个支持协程的类型,通常是 std::futurestd::generator 或自定义的 Task 类型。C++20 标准库中提供了 std::futurestd::generator,但它们并不是协程的唯一实现方式。

std::future <int> async_add(int a, int b);

2.2 co_await 的使用

co_await 关键字需要与一个可等待对象配合使用。可等待对象需要实现 await_ready(), await_suspend(), await_resume() 三个成员函数。标准库中的 std::future 就满足此接口。

auto result = co_await some_future;

2.3 co_yield 的使用

co_yield 用于生成器(generator)场景,每次调用会返回一个值并挂起。

co_yield 42;

2.4 自定义协程类型

下面给出一个最小的 Task 协程包装器示例:

struct Task {
    struct promise_type {
        int result_;
        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 return_value(int v) { result_ = v; }
        void unhandled_exception() { std::terminate(); }
    };
    std::coroutine_handle <promise_type> coro_;
    int get() { return coro_.promise().result_; }
};

3. 实际使用案例

3.1 异步网络请求

假设我们使用一个异步 HTTP 库(例如 cpp-httplib 的异步 API):

#include <iostream>
#include <cpprest/http_client.h> // 用于示例,实际项目中可替换为任意异步库

Task fetch_url(const std::string& url) {
    auto client = std::make_shared <http_client>(url);
    auto resp = co_await client->request(methods::GET);
    std::string body = co_await resp.extract_string();
    co_return body.size();
}

3.2 并行计算

协程可以与 std::async 或自定义线程池配合,实现并行计算:

Task parallel_sum(const std::vector <int>& data) {
    int sum1 = 0, sum2 = 0;
    auto fut1 = std::async(std::launch::async, [&]() {
        for (int i = 0; i < data.size() / 2; ++i) sum1 += data[i];
    });
    auto fut2 = std::async(std::launch::async, [&]() {
        for (size_t i = data.size() / 2; i < data.size(); ++i) sum2 += data[i];
    });
    co_await fut1;
    co_await fut2;
    co_return sum1 + sum2;
}

3.3 生成器模式

使用 co_yield 可以轻松实现懒加载的序列:

std::generator <int> range(int start, int end) {
    for (int i = start; i < end; ++i)
        co_yield i;
}

使用时:

for (int n : range(0, 10)) {
    std::cout << n << ' ';
}

4. 性能与注意事项

  • 上下文切换成本:协程的挂起/恢复不涉及内核上下文切换,成本较低。但若协程内部仍然使用阻塞调用,效果有限。
  • 内存占用:协程的状态机会生成额外的数据结构,编译器会将其布局在堆栈或堆中。对于长生命周期的协程,建议使用自定义的 promise_type 来控制堆栈大小。
  • 异常传播co_awaitco_yield 的异常需要在 promise_typeunhandled_exception 中处理,否则会调用 std::terminate()

5. 小结

C++20 的协程为现代 C++ 开发提供了一种统一且高效的异步编程模型。通过 co_awaitco_yield 等关键字,开发者可以写出更接近同步逻辑的代码,同时享受并发执行带来的性能优势。掌握协程的基本语法、实现细节以及与现有库的配合方式,将大幅提升项目的可维护性和开发效率。祝你在 C++ 生态中玩得愉快,写出更优雅、更高效的代码!

发表评论