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

在 C++20 之前,异步编程往往需要使用线程、回调或者第三方库(如 Boost.Asio、libuv 等)来实现。C++20 标准通过引入协程(coroutine)语法,提供了一种更直观、更高效的异步编程模型。本文将带你快速了解协程的核心概念、实现机制以及在实际项目中的应用场景。


1. 什么是协程?

协程是可以在执行过程中“挂起”和“恢复”的函数。与线程不同,协程在单个线程内切换,只占用少量栈空间,并且不需要像线程那样昂贵的上下文切换。协程可以被看作是把函数拆分成若干可暂停的步骤,每一步都可以被外部调度器控制。

  • 挂起co_awaitco_yieldco_return):函数在此处暂停执行,返回一个值或等待一个异步操作完成。
  • 恢复:当外部调度器决定继续执行协程时,协程从挂起点恢复。

2. 协程的核心组件

C++20 协程的实现依赖于以下关键概念:

关键字 说明 典型用法
co_await 等待一个异步操作完成,挂起协程 auto result = co_await async_io();
co_yield 产生一个值,挂起协程 co_yield value;
co_return 结束协程,返回最终结果 co_return final_value;
std::suspend_always / std::suspend_never 控制协程是否立即挂起 co_await std::suspend_always{};

协程需要一个 promise type(承诺类型)来描述挂起、恢复以及返回值等行为。C++20 标准库提供了一些默认实现(如 std::promisestd::future),但在实际项目中我们通常会自定义 promise_type 以满足业务需求。


3. 一个简单的协程示例

下面的例子演示了如何使用协程实现一个异步计数器。它在每次计数后挂起,等待 1 秒钟后恢复,最终返回总计数值。

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

// 1. 定义一个简单的异步延迟类型
struct Delay {
    struct promise_type {
        std::chrono::milliseconds wait_time;
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
    using handle_type = std::coroutine_handle <promise_type>;
    handle_type coro;

    Delay(std::chrono::milliseconds ms) : coro(handle_type::from_promise(*new promise_type{ms})) {}
    ~Delay() { coro.destroy(); }

    void resume() {
        std::this_thread::sleep_for(coro.promise().wait_time);
        coro.resume();
    }
};

// 2. 计数器协程
struct Counter {
    struct promise_type {
        int count = 0;
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        int get_return_object() { return count; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
    using handle_type = std::coroutine_handle <promise_type>;
    handle_type coro;

    Counter(handle_type h) : coro(h) {}
    ~Counter() { coro.destroy(); }

    int get_value() const { return coro.promise().count; }

    // 计数函数
    static Counter run(int max) {
        for (int i = 1; i <= max; ++i) {
            co_await Delay(1000ms);   // 每秒等待一次
            co_yield i;               // 暂停并产生当前计数
            co_await std::suspend_always{}; // 让外部恢复
        }
        co_return; // 结束协程
    }
};

int main() {
    auto counter = Counter::run(5);
    while (counter.coro) {
        counter.coro.resume();       // 恢复协程
        std::cout << "Count: " << counter.get_value() << '\n';
    }
    std::cout << "Final count: " << counter.get_value() << '\n';
}

运行效果(每秒打印一次):

Count: 1
Count: 2
Count: 3
Count: 4
Count: 5
Final count: 5

该示例展示了协程挂起与恢复的基本流程。Delay 用来模拟异步等待,Counter 在每一次 co_yield 后挂起,外部通过 coro.resume() 恢复。


4. 协程在 IO 编程中的应用

协程最常用于网络 IO 或磁盘 IO。结合异步 I/O API(如 libuvasio),可以让异步操作像同步代码一样编写。下面是一个使用 asio 的简化示例(不完整):

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

asio::awaitable <void> tcp_client() {
    asio::ip::tcp::socket socket(co_await asio::this_coro::executor);
    co_await socket.async_connect(asio::ip::tcp::endpoint(asio::ip::make_address("127.0.0.1"), 8080), asio::use_awaitable);
    std::string msg = "Hello, server!\n";
    co_await asio::async_write(socket, asio::buffer(msg), asio::use_awaitable);
    // 读取响应
    std::array<char, 1024> buf;
    std::size_t n = co_await asio::async_read(socket, asio::buffer(buf), asio::use_awaitable);
    std::cout << "Received: " << std::string(buf.data(), n) << '\n';
    socket.close();
}

优势

  • 可读性:异步流程像同步代码,易于维护。
  • 性能:避免线程上下文切换,协程本身非常轻量。
  • 资源利用:单线程即可处理大量并发连接。

5. 常见坑与最佳实践

  1. 不要在协程里直接使用 std::thread:协程本身已经能处理并发,加入线程会增加复杂度。
  2. 确保 promise_type 的生命周期:如果协程返回 std::future 或自定义类型,必须保证 promise_type 在协程结束后仍然有效。
  3. 异常处理:使用 unhandled_exception 把异常转为 std::terminate,或者自定义异常捕获逻辑。
  4. 调试难度:调试协程时,调试器可能会在挂起点停下,了解 awaitable 的执行顺序非常重要。

6. 小结

C++20 协程为异步编程带来了革命性的简化。通过 co_awaitco_yieldco_return,我们可以像写同步代码一样描述异步流程,极大提升代码可读性与可维护性。结合现有异步库(如 asiolibuv)或者自行实现轻量异步 I/O,协程在高性能网络服务、实时数据处理等领域已成为主流技术。

如果你正在寻找一种更高效、更易维护的异步编程方式,C++20 协程绝对值得一试。祝你编码愉快!

发表评论