C++17中的协程:从协作式到异步编程的跃迁

C++20正式加入协程(coroutines)后,协程在语言层面得到了标准化,使得实现异步、生成器等复杂控制流变得异常简单。本文将带你从C++17的协程实验性实现说起,探讨协程的底层原理、常见用法以及与传统异步模型(如线程+future、回调)的区别与优劣。

1. 何为协程?

协程是一种轻量级的用户级线程,允许函数在执行过程中“挂起”(yield)并在需要时恢复。与线程不同,协程的切换是由程序显式控制的,几乎不需要上下文切换开销。协程分为三类:

  • 生成器:每次返回一个值后挂起;
  • 异步:等待某些事件完成后恢复;
  • 协作式:多个协程在运行时通过显式 co_awaitco_yield 进行切换。

2. C++17 的协程实验性实现

C++17 为协程提供了“预编译器”功能,主要通过 coro.h 头文件实现,编译器支持 co_awaitco_yieldco_return。然而该实验版本不完整,缺少关键类型(如 std::experimental::coroutine_handle)的完整实现,且只能通过第三方库(如 Boost.Coroutine2)来使用。

典型的实验性协程代码(需 Boost.Coroutine2):

#include <boost/coroutine2/all.hpp>
#include <iostream>

void generator(boost::coroutines2::coroutine <void>::push_type &sink) {
    for (int i = 0; i < 5; ++i) {
        sink(i); // co_yield 的实现
    }
}

int main() {
    boost::coroutines2::coroutine <void>::pull_type source(
        [](boost::coroutines2::coroutine <void>::push_type &sink) {
            generator(sink);
        });

    while (source) {
        std::cout << source.get() << '\n';
        ++source; // 手动切换
    }
}

虽然能实现生成器,但缺乏语言级语法糖和更复杂的异步功能。

3. C++20 协程的完整实现

C++20 提供了标准化的协程库,核心类为 std::coroutine_handle,并引入了 std::suspend_alwaysstd::suspend_never 等默认挂起策略。协程函数的返回类型必须是支持 co_awaitawaitable

3.1 生成器实现

#include <coroutine>
#include <iostream>

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

    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{handle_type::from_promise(*this)};
        }
        void unhandled_exception() { std::exit(1); }
        void return_void() {}
    };

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

    struct iterator {
        handle_type coro;
        bool operator==(std::default_sentinel_t) const noexcept { return !coro || coro.done(); }
        iterator &operator++() { coro.resume(); return *this; }
        T operator*() const { return coro.promise().current_value; }
    };

    iterator begin() { return iterator{coro}; }
    std::default_sentinel_t end() { return {}; }
};

generator <int> range(int start, int end) {
    for (int i = start; i <= end; ++i)
        co_yield i;
}

int main() {
    for (auto v : range(1, 5))
        std::cout << v << ' ';
}

这段代码几乎与传统循环一样直观,且编译器自动管理协程栈。

3.2 异步任务(async)

C++20 标准库中 std::future 已被 std::future(已支持异步)与 std::promise 结合使用。协程可以返回 std::future,使用 co_await 等待异步结果。

#include <coroutine>
#include <future>
#include <iostream>

std::future <int> async_add(int a, int b) {
    co_return a + b;
}

int main() {
    auto fut = async_add(3, 4);
    std::cout << "Result: " << fut.get() << '\n';
}

此处 co_return 自动将结果封装为 std::future,无需手动包装。

4. 协程与传统异步模型的对比

特点 协程 线程+future 回调
切换成本 轻量级,栈已在堆上 上下文切换开销大 需要手动维护状态机
可读性 接近同步代码 可读性一般 嵌套回调导致“回调地狱”
错误处理 通过 try/catch 统一 同样可使用 try/catch 需在每层回调中处理
资源管理 通过 RAII 自动 需显式锁/同步 难以保证释放

协程在高并发 I/O 密集型程序(如网络服务器)中表现尤为突出:相比起大量线程,它们占用更少资源并保持代码直观。

5. 常见坑与最佳实践

  1. 栈大小限制
    虽然协程的栈在堆上,但若协程内部递归深度过大,仍会导致堆栈耗尽。应避免深递归或使用循环代替。

  2. 异常传播
    若协程内部抛出异常,co_return 不会捕获,需要在 promise_type::unhandled_exception 中处理。

  3. 同步与异步的混用
    同步函数调用异步协程时,要注意 co_await 的上下文。最好在统一的事件循环中执行协程。

  4. 调试工具
    目前主流 IDE 对协程调试支持有限,建议使用 -fno-omit-frame-pointer 编译参数,并配合 llvm-symbolizer

6. 结语

C++20 的协程为现代 C++ 开发者提供了一种既轻量又高效的并发编程模型。通过把异步逻辑写成看似同步的代码,降低了复杂性并提升了可维护性。虽然协程在 I/O 密集型场景中表现突出,但在 CPU 密集型计算时,线程仍是更优选择。掌握协程的使用后,你将能写出更简洁、更高性能的 C++ 程序。

发表评论