C++ 中的协程:从 Boost 到 C++20

协程(Coroutine)是实现异步编程的一种强大机制,它让我们能够在单线程中写出看似同步、但实际运行时是非阻塞的代码。C++ 通过标准化协程(C++20 起)与 Boost 等第三方库提供了完整的协程生态,使得异步编程变得更为直观和高效。本文将从协程的基本概念、实现方式、以及在现代 C++ 项目中的实际应用来展开讨论。

1. 协程的基本概念

协程是一种比线程更轻量级的计算单元。与线程不同,协程共享同一线程的栈空间,在执行时可以暂停(co_awaitco_yield)并在需要时恢复。协程的暂停与恢复由编译器生成的状态机来管理,程序员只需要关注业务逻辑即可。

协程的核心语义可以归纳为:

  • 挂起(suspend):协程在执行过程中遇到 co_awaitco_yieldco_return 时会挂起,返回给调用者。
  • 恢复(resume):调用者或事件循环触发协程恢复执行,直至再次挂起或结束。

2. Boost.Coroutine 与 Boost.Asio

在 C++20 标准化之前,Boost.Coroutine 提供了两种协程实现:

  • 协作式协程:使用 boost::coroutines::coroutine,适合单线程协程的场景。
  • 协作式异步协程:结合 boost::asioasync_* 函数,支持 I/O 异步操作。

Boost.Asio 通过 async_* 函数配合 io_context 实现了事件驱动的异步 I/O。典型的使用方式如下:

#include <boost/asio.hpp>

void async_read(boost::asio::ip::tcp::socket& socket, std::vector <char>& buffer) {
    socket.async_read_some(boost::asio::buffer(buffer),
        [](boost::system::error_code ec, std::size_t bytes_transferred){
            if (!ec) {
                // 处理数据
            }
        });
}

通过回调函数的形式,Boost.Asio 实现了协程式的异步编程模型。虽然回调层数较多,但 Boost.Asio 的性能与灵活性在实际项目中得到广泛验证。

3. C++20 标准协程

C++20 对协程的支持主要体现在以下几个关键特性:

  • co_await:用于挂起协程,等待一个 awaitable 对象完成。
  • co_yield:产生一个值并挂起,适用于生成器模式。
  • co_return:返回协程最终结果并结束协程。
  • std::coroutine_handle:底层句柄,用于控制协程的生命周期。

标准协程需要实现一个 awaitable 类型,典型的实现需要包含:

struct awaitable {
    bool await_ready() noexcept { /* ... */ }
    void await_suspend(std::coroutine_handle<> h) noexcept { /* ... */ }
    T await_resume() noexcept { /* ... */ }
};

使用标准协程实现一个简单的异步 I/O 例子:

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

struct task {
    struct promise_type {
        task get_return_object() { return {}; }
        std::suspend_never initial_suspend() noexcept { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void unhandled_exception() noexcept {}
        void return_void() noexcept {}
    };
};

task async_sleep(std::chrono::milliseconds ms) {
    std::this_thread::sleep_for(ms);
    co_return;
}

int main() {
    async_sleep(1000);
    std::cout << "Finished sleeping\n";
}

虽然上例只是同步阻塞,但它演示了协程语法。真正的异步 I/O 需要将 std::this_thread::sleep_for 替换为非阻塞等待,例如与 asio 或自定义事件循环结合。

4. 生成器模式:co_yield 的魅力

co_yield 让协程可以像迭代器一样产出一系列值,极大简化了生成器的实现。例如,生成斐波那契数列:

#include <coroutine>
#include <iostream>

struct generator {
    struct promise_type {
        int current_value;
        generator get_return_object() { return {}; }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(int value) noexcept {
            current_value = value;
            return {};
        }
        void return_void() noexcept {}
        void unhandled_exception() noexcept {}
    };
    struct iterator {
        std::coroutine_handle <promise_type> coro;
        int value;
        iterator(std::coroutine_handle <promise_type> h) : coro(h) {
            if (coro)
                value = coro.promise().current_value;
        }
        iterator& operator++() {
            coro.resume();
            if (coro.done()) coro = nullptr;
            else value = coro.promise().current_value;
            return *this;
        }
        int operator*() const { return value; }
        bool operator==(std::default_sentinel_t) const { return !coro; }
    };
    iterator begin() {
        auto h = std::coroutine_handle <promise_type>::from_promise(*this);
        h.resume();
        return iterator(h);
    }
    std::default_sentinel_t end() { return {}; }
};

generator fibonacci(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield a;
        int next = a + b;
        a = b;
        b = next;
    }
}

int main() {
    for (auto x : fibonacci(10))
        std::cout << x << ' ';
    std::cout << '\n';
}

运行结果为:0 1 1 2 3 5 8 13 21 34co_yield 的实现让生成器的写法与 std::vector 的使用方式一脉相承,代码简洁且易于维护。

5. 与 std::futurestd::promise 的区别

传统的 std::future / std::promise 也支持异步结果传递,但它们是基于线程/任务的同步机制,无法做到协程内部的挂起/恢复。协程通过 co_await 对 awaitable 对象进行挂起,整个过程不涉及额外线程,降低了上下文切换成本。

此外,std::futureget() 会阻塞,除非使用 wait_forwait_until。而协程的 await_resume() 在挂起对象完成后直接返回值,保持了异步非阻塞的本质。

6. 实际项目中的协程使用技巧

  1. 与 IO 框架配合
    在网络编程中,将协程与事件循环框架(如 asio::io_contextlibuv 或自研 loop)结合,使用 co_await 等待异步事件完成。这样可以避免回调地狱,使代码保持同步式结构。

  2. 错误处理
    协程内的异常可以通过 try-catch 捕获,并在 await_resume() 中重新抛出或返回错误码。std::exception_ptr 可用于跨协程传播异常。

  3. 性能调优

    • 只在真正需要异步 I/O 的地方使用协程。
    • 通过 std::suspend_always / std::suspend_never 控制挂起点,避免不必要的上下文切换。
    • 在生成器中尽量使用 co_yield 产生的值进行惰性计算,避免一次性生成大量数据导致内存占用。
  4. 协程池
    对于需要大量短生命周期协程的场景,可实现协程池或协程任务调度器,以复用协程句柄和减少堆栈分配。

7. 未来趋势

C++ 标准库已经为协程奠定了基础,但真正的异步编程仍然依赖于成熟的 I/O 库与事件循环。随着 C++23 与后续标准的推出,协程相关的工具(如 std::generatorstd::taskstd::coroutine_traits)将进一步完善,语言层面也会提供更多便利的语法糖。

8. 结语

协程让 C++ 的异步编程从回调到同步式代码变得自然。借助 Boost 及 C++20 标准提供的协程机制,程序员可以在保持代码可读性的同时,充分利用系统资源,构建高性能、高可扩展性的应用。无论是网络服务器、游戏引擎还是大数据处理,协程都是不可或缺的技术武器。欢迎大家在项目中大胆尝试并分享经验,共同推动 C++ 异步编程的落地与成熟。

发表评论