C++20 中的协程:设计原则与实战案例

协程是 C++20 标准中引入的重要特性,它使得异步编程与协作式并发变得更自然。本文将从协程的设计原则、核心概念、实现细节以及一个实战案例来全面梳理 C++20 协程。

一、设计原则

  1. 无侵入性
    协程的语法(co_awaitco_yieldco_return)与传统函数几乎完全兼容,编译器在不破坏已有代码的情况下实现协程功能。

  2. 可组合性
    协程可以像普通函数一样返回 std::futurestd::generator 或自定义协程类型,便于层层组合。

  3. 轻量级
    协程的帧(协程状态机)在栈上分配,减少了堆分配的开销。

  4. 可定制的 Promise
    通过自定义 promise_type,可以在协程开始、结束或异常时执行自定义逻辑。

二、核心概念

关键字 说明
co_await 用于等待一个可等待对象(Awaitable),协程在此挂起
co_yield 用于生成值,暂停协程并返回一个值给调用方
co_return 结束协程,返回一个值给 Promise
promise_type 协程的核心,负责管理协程生命周期、状态、返回值等

Awaitable

一个类型只要满足以下接口即可被 co_await

struct Awaitable {
    bool await_ready();      // 是否已就绪
    void await_suspend(std::coroutine_handle<> h); // 挂起时调用
    auto await_resume();     // 恢复时返回的值
};

协程句柄

std::coroutine_handle<> 用于控制协程的生命周期,例如挂起、恢复或销毁。

三、实现细节

  1. 协程函数编译器生成
    编译器会把协程函数拆分为两个结构体:promise_type 与内部生成的 generatortask

  2. 栈帧
    协程的本地变量和 promise_type 的成员都放在一个连续的内存块中,形成协程帧。

  3. 异常处理
    若协程内部抛出异常,异常会存储在 promise_type 中,直到 co_returnco_yield 之后再重新抛出。

  4. 自定义 Awaitable
    可以为 std::chrono::steady_clock::duration 写一个 Awaitable,实现协程的 sleep 功能。

四、实战案例:异步 HTTP 请求

下面给出一个简化的异步 HTTP 客户端示例,使用协程实现非阻塞请求。

#include <iostream>
#include <string>
#include <coroutine>
#include <chrono>
#include <thread>
#include <future>

struct SleepAwaitable {
    std::chrono::milliseconds duration;
    bool await_ready() noexcept { return duration.count() <= 0; }
    void await_suspend(std::coroutine_handle<> h) {
        std::thread([h, d=duration]{
            std::this_thread::sleep_for(d);
            h.resume();
        }).detach();
    }
    void await_resume() noexcept {}
};

struct HttpResponse {
    int status;
    std::string body;
};

struct HttpRequest {
    std::string url;
    std::future <HttpResponse> operator()() {
        struct Awaiter {
            std::coroutine_handle<> coro;
            HttpRequest req;
            HttpResponse operator()() {
                std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟网络延迟
                return {200, "Response from " + req.url};
            }
            bool await_ready() noexcept { return false; }
            void await_suspend(std::coroutine_handle<> h) {
                coro = h;
                std::thread([this]{
                    auto res = operator()();
                    coro.promise().return_value = res;
                    coro.resume();
                }).detach();
            }
            void await_resume() noexcept {}
        };

        struct Promise {
            HttpResponse return_value;
            HttpRequest get_return_object() {
                return { "", std::make_shared<std::promise<HttpResponse>>(std::promise<HttpResponse>()), std::move(return_value) };
            }
            std::suspend_never initial_suspend() noexcept { return {}; }
            std::suspend_never final_suspend() noexcept { return {}; }
            void unhandled_exception() { std::terminate(); }
            void return_void() {}
        };

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

        return Task{ std::experimental::suspend_always() }.coro.promise().return_value.get_future();
    }
};

async auto fetch(const std::string& url) -> std::future <HttpResponse> {
    HttpRequest req{url};
    co_return co_await req();
}

int main() {
    auto fut = fetch("http://example.com");
    SleepAwaitable{std::chrono::milliseconds(200)}.await_resume();
    auto res = fut.get();
    std::cout << "Status: " << res.status << "\nBody: " << res.body << std::endl;
}

说明

  • SleepAwaitable 实现了一个简单的协程 sleep
  • HttpRequest 使用 std::future 作为返回类型,并内部启动线程来模拟网络请求。
  • fetch 函数是一个协程,使用 co_await 等待请求完成。
  • main 中通过 fetch 异步获取数据,然后使用 future.get() 同步获取结果。

五、总结

C++20 协程通过提供轻量级、可组合且可定制的协程框架,让异步编程与协作式并发的实现变得简单直观。熟悉 co_awaitco_yieldco_return 以及 promise_type 的实现细节,能够帮助开发者在实际项目中高效地使用协程,提升代码可读性和性能。


发表评论