C++20 协程的使用与实践

协程(Coroutines)是 C++20 标准新增的一项功能,旨在让异步编程变得更直观、更高效。它们可以在执行过程中“挂起”并在稍后恢复,内部维护一个状态机,从而让代码保持同步写法,减少回调地狱。下面我们从概念、语法、实现细节和实际应用四个方面深入探讨 C++20 协程。

1. 协程的基本概念

  • 挂起点(Suspension Point):代码执行到 co_awaitco_yieldco_return 时会挂起协程。
  • 恢复点(Resumption Point):协程被调用或外部事件触发后恢复执行。
  • 协程句柄(std::coroutine_handle:用于手动控制协程的生命周期、挂起和恢复。
  • 协程类型:C++20 通过返回类型的特殊属性 generatortaskfuture 等来区分不同用途的协程。

2. 协程的语法要点

2.1 基本语法

#include <coroutine>
#include <iostream>
#include <string_view>

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

    std::coroutine_handle <promise_type> handle;

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

    bool next() {
        if (!handle.done()) handle.resume();
        return !handle.done();
    }

    T value() const { return handle.promise().current_value; }
};

2.2 用法示例

generator <int> count(int start, int end) {
    for (int i = start; i <= end; ++i) {
        co_yield i;            // 生成一个值并挂起
    }
}

int main() {
    for (auto g = count(1, 5); g.next(); ) {
        std::cout << g.value() << ' ';
    }
}

输出 1 2 3 4 5

3. 任务协程(task

任务协程更适合异步 I/O 或长时间运行的操作。下面演示一个简单的异步函数:

#include <coroutine>
#include <exception>
#include <iostream>

struct task {
    struct promise_type {
        std::exception_ptr eptr;

        task get_return_object() { return task{ std::coroutine_handle <promise_type>::from_promise(*this) }; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { eptr = std::current_exception(); }
        void return_void() {}
    };

    std::coroutine_handle <promise_type> handle;

    explicit task(std::coroutine_handle <promise_type> h) : handle(h) {}
    ~task() { if (handle) handle.destroy(); }

    void resume() { handle.resume(); }
};

task async_io_simulation() {
    std::cout << "开始异步操作...\n";
    co_await std::suspend_always(); // 模拟 I/O 等待
    std::cout << "异步操作完成!\n";
}

int main() {
    auto t = async_io_simulation();
    t.resume(); // 恢复到第一暂停点
    t.resume(); // 继续到最后
}

4. 与标准库协同

C++23 继续扩展协程的生态,加入 std::ranges::subrangestd::generator 等适配器。即使在 C++20,也可以配合 std::futurestd::promise 或第三方库如 Boost.Asio 使用协程。

5. 性能与常见误区

误区 解释
协程会导致大量堆分配 协程帧默认放在栈上,除非使用 co_yieldco_await 的 awaiter 需要堆分配。
协程只能用于 I/O 协程可以用于任何需要挂起与恢复的场景,如生成器、事件循环、状态机等。
所有协程都需要手动销毁 只要返回类型定义了 final_suspend 并返回 std::suspend_always,协程句柄在退出时会自动销毁。

6. 实战案例:异步 HTTP 请求

下面用 cpprestsdk(Casablanca)演示一个异步 HTTP GET 请求的协程实现。

#include <cpprest/http_client.h>
#include <cpprest/filestream.h>
#include <iostream>

using namespace web::http::client;
using namespace utility::conversions; // for to_string_t

task <void> fetch_and_print(const std::string& url) {
    http_client client(to_string_t(url));
    auto response = co_await client.request(methods::GET);
    std::cout << "HTTP Status: " << response.status_code() << '\n';
    auto body = co_await response.extract_string();
    std::cout << "Body: " << body << '\n';
}

调用方式:

int main() {
    auto t = fetch_and_print("http://www.example.com");
    t.resume(); // 执行请求
}

7. 结语

C++20 的协程为语言带来了更清晰、可维护的异步编程范式。掌握协程的核心概念、语法和生命周期管理后,你可以轻松地将其应用于生成器、状态机、网络 I/O 等多种场景。随着标准库的完善(C++23 进一步提供了 std::generator 等)和社区生态的丰富,协程正逐渐成为 C++ 开发者的强大工具。

祝你在协程世界里玩得开心,写出更高效、更优雅的 C++ 代码!

发表评论