C++ 20 新特性:协程的实践与优化

在 C++ 20 版本中,协程(coroutines)被正式纳入标准库,提供了一套完整而高效的异步编程模型。相比传统的回调或线程池,协程让异步代码更易读、易维护。本文将介绍协程的基本概念、关键类、常见使用场景,并提供一份可直接运行的示例代码,帮助你快速上手。

一、协程的基本概念

协程是一种轻量级的函数,它在执行过程中可以被暂停(co_awaitco_yield),随后在需要时恢复。协程内部的状态会被保存在协程帧(coroutine frame)中,编译器会为协程生成一套隐藏的状态机。

1.1 关键关键字

  • co_await:等待一个 awaitable 对象完成,并在完成后恢复协程。
  • co_yield:将一个值返回给调用者,挂起协程,等待下一次调用。
  • co_return:返回一个最终值,结束协程。

1.2 awaitable 与 awaiter

协程需要等待的对象必须满足 awaitable 接口。最常见的是 std::futurestd::shared_futurestd::promise 或自定义的 awaitable。awaitable 会产生一个 awaiter 对象,awaiter 必须提供 await_ready()await_suspend()await_resume() 三个成员。

二、标准库中的协程工具

C++ 20 标准库提供了一些协程相关的模板,简化了常见需求。

模板 作用 典型使用场景
`std::generator
| 用于实现可迭代的协程,支持co_yield` 生成序列、懒加载
`std::task
| 用于异步任务,支持co_return` IO、网络请求
std::suspend_always / std::suspend_never 简单的挂起策略 基础控制
std::experimental::generator 早期实现,兼容旧编译器 兼容性

三、协程的实践示例

下面给出一个完整的协程实现示例:一个异步下载器,使用 std::task<std::string> 下载网页内容,并在下载完成后打印。我们通过自定义 simple_http_get 来模拟异步 HTTP 请求。

#include <iostream>
#include <string>
#include <chrono>
#include <thread>
#include <future>
#include <coroutine>
#include <experimental/generator>
#include <optional>

// 简单的 awaitable:模拟异步 HTTP GET
struct async_http_get {
    std::string url;
    std::optional<std::string> result;

    struct awaiter {
        async_http_get& self;
        bool await_ready() const noexcept { return false; }
        void await_suspend(std::coroutine_handle<> h) noexcept {
            std::thread([this, h]() {
                // 模拟网络延迟
                std::this_thread::sleep_for(std::chrono::seconds(2));
                self.result = "Content from " + self.url;
                h.resume(); // 继续执行协程
            }).detach();
        }
        std::string await_resume() const noexcept { return *self.result; }
    };

    awaiter operator co_await() { return { *this }; }
};

// 异步任务返回字符串
template<typename T>
struct task {
    struct promise_type {
        T value_;
        std::exception_ptr eptr_;
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_value(T val) { value_ = std::move(val); }
        void unhandled_exception() { eptr_ = std::current_exception(); }
        task get_return_object() {
            return task{ std::coroutine_handle <promise_type>::from_promise(*this) };
        }
    };
    std::coroutine_handle <promise_type> coro_;
    task(std::coroutine_handle <promise_type> h) : coro_(h) {}
    task(task&& other) noexcept : coro_(std::exchange(other.coro_, {})) {}
    ~task() { if (coro_) coro_.destroy(); }

    std::future <T> result() {
        return std::async(std::launch::async, [this]() {
            if (coro_.promise().eptr_) std::rethrow_exception(coro_.promise().eptr_);
            return coro_.promise().value_;
        });
    }
};

// 协程函数:异步下载
task<std::string> download(const std::string& url) {
    async_http_get req{ url };
    std::string body = co_await req; // 等待下载完成
    co_return body;
}

int main() {
    auto fut = download("https://example.com").result();
    std::cout << "Download started...\n";
    std::cout << "Result: " << fut.get() << stdn::endl;
    return 0;
}

运行结果示例

Download started...
Result: Content from https://example.com

该示例演示了:

  1. 自定义 awaitable (async_http_get) 并实现 awaiter 接口。
  2. 定义通用 `task ` 模板,包装协程返回值与异常。
  3. 在主函数中启动协程并通过 std::future 方式等待结果。

四、协程性能与最佳实践

  • 减少栈帧大小:协程帧会在堆上分配,使用 co_yield 产生大量值时可考虑 std::generator,减少复制成本。
  • 错误传播:在 promise_type 中使用 unhandled_exception() 捕获异常,并通过 std::future 传播。
  • 线程安全:若协程跨线程,使用 std::atomic 或互斥锁保护共享状态。
  • 使用第三方库:如 cppcorofollylibuv 的协程实现,提供更丰富的 IO、网络、线程池支持。

五、结语

C++ 20 的协程为异步编程带来了更接近同步代码的可读性和维护性。掌握 awaitable 接口、标准库协程工具以及正确的错误处理策略,你就能在 C++ 项目中快速构建高效、可扩展的异步功能。欢迎在评论区分享你使用协程的经验或遇到的坑!

发表评论