C++20中的协程:从概念到实践

在C++20中,协程(coroutine)被正式加入标准库,提供了一种轻量级的异步编程模型。相比传统的回调或多线程,协程可以在单线程中实现高效、可读性强的异步代码。本文将从协程的基本概念开始,逐步展示如何在实际项目中使用协程,并解决常见问题。

一、协程基础

1. 协程是什么?

协程是一种程序执行单元,支持在任意位置挂起(co_awaitco_yieldco_return)并在以后恢复。它们的特点是:

  • 轻量级:不像线程那样需要堆栈和调度开销。
  • 可暂停:执行可以在任何点暂停,等待某个事件。
  • 易读性:代码流类似同步代码,避免回调地狱。

2. 关键字

  • co_await:等待一个可等待对象(awaitable),类似 await
  • co_yield:生成一个值,类似生成器。
  • co_return:返回一个值并结束协程。

3. Awaitable 类型

要让对象可以被 co_await,必须满足以下接口:

bool await_ready();   // 是否立即完成
void await_suspend(std::coroutine_handle<>) ; // 挂起
auto await_resume();  // 结果

二、协程的实现

1. 自定义 awaitable

下面实现一个简单的异步延时:

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

Sleep sleep_for(std::chrono::milliseconds ms) {
    return Sleep{ms};
}

2. Coroutine Promise

协程需要一个 promise 对象,用来管理返回值和异常。最常见的是 std::future/std::promise 的组合,但 C++20 提供了更通用的 std::generatorstd::async 结构。

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

template<typename T>
struct Task {
    struct promise_type;
    using handle_type = std::coroutine_handle <promise_type>;

    struct promise_type {
        T value_;
        std::exception_ptr ex_;

        Task get_return_object() {
            return Task{handle_type::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { ex_ = std::current_exception(); }
        void return_value(T v) { value_ = v; }

        // 可选:允许 co_await
        T await_resume() {
            if (ex_) std::rethrow_exception(ex_);
            return value_;
        }
    };

    handle_type h_;
    explicit Task(handle_type h) : h_(h) {}
    ~Task() { if (h_) h_.destroy(); }
    T get() {
        if (!h_.done()) h_.resume();
        return h_.promise().await_resume();
    }
};

3. 示例:异步网络请求

假设我们使用 asio(Boost.Asio 或 standalone asio)来发起异步 HTTP 请求,下面给出一个简化示例:

#include <asio.hpp>
#include <iostream>

Task<std::string> async_http_get(asio::io_context& io, const std::string& host, const std::string& path) {
    asio::ip::tcp::resolver resolver(io);
    auto endpoints = co_await resolver.async_resolve(host, "http", asio::use_awaitable);
    asio::ip::tcp::socket socket(io);
    co_await asio::async_connect(socket, endpoints, asio::use_awaitable);

    std::string request = "GET " + path + " HTTP/1.1\r\n"
                          "Host: " + host + "\r\n"
                          "Connection: close\r\n\r\n";
    co_await asio::async_write(socket, asio::buffer(request), asio::use_awaitable);

    std::string response;
    asio::streambuf buffer;
    for (;;) {
        std::size_t n = co_await asio::async_read(socket, buffer, asio::transfer_at_least(1), asio::use_awaitable);
        std::istream is(&buffer);
        std::string chunk;
        std::getline(is, chunk, '\0');
        response += chunk;
        if (n < 1) break; // 结束读取
    }
    co_return response;
}

三、协程的优势与注意事项

1. 优势

  • 代码可读性:协程让异步代码呈现同步写法。
  • 资源消耗低:协程内部仅保留必要状态,不需要系统线程堆栈。
  • 组合灵活:可以组合 co_awaitco_yield,实现流式处理。

2. 注意事项

  • 异常处理:协程内部的异常会被 promise 捕获,必须在 await_resume() 中重新抛出或处理。
  • 生命周期:协程句柄持有 promise,需要确保对象在使用前未被销毁。
  • 调试支持:IDE 可能不支持调试协程中的暂停点,需要使用日志或断点替代。

四、最佳实践

  1. 使用标准库协程包装
    C++20 标准提供 std::generatorstd::future 的协程版本,尽量使用官方实现,避免手写 Promise 逻辑。

  2. 尽量避免阻塞
    在协程中不要使用 std::this_thread::sleep_for 等阻塞调用,改为 co_await 形式的异步等待。

  3. 错误恢复
    通过 try/catch 包裹协程逻辑,并使用 co_return 返回错误码或错误对象。

  4. 资源释放
    确保协程结束时资源被释放,利用 RAII 与协程内 co_await 结合实现。

五、结语

C++20 的协程为异步编程带来了革命性的改变,让代码更简洁、更易维护。掌握协程的基本语法、awaitable 对象的实现以及与第三方库的配合,是现代 C++ 开发者不可或缺的技能。希望本文能帮助你在项目中快速落地协程,开启更高效、可读的异步代码新时代。

发表评论