C++ 17 中的协程(coroutine)如何提升异步编程效率

协程是 C++20 标准才正式加入的特性,C++17 通过 std::experimental::coroutine 预先实现了协程的框架。虽然在 C++17 里协程还属于实验阶段,但它已经展示了如何用更直观的方式写异步代码,减少回调地狱、提升代码可读性与维护性。本文将从协程的基本概念、关键字与语法、典型使用场景以及如何在 C++17 项目中引入协程进行阐述。

一、协程的基本概念

协程(coroutine)是一种轻量级的用户级线程,它可以在函数执行中随时挂起(yield)并恢复(resume),而不是像线程那样频繁地被操作系统调度。协程的优势体现在:

  1. 挂起与恢复:协程可以在任意位置挂起,随后从同一点继续执行。
  2. 状态保留:协程挂起时保留局部变量状态,恢复时无需重新创建栈。
  3. 更好的性能:相比多线程,协程切换更快,开销更小。
  4. 编写顺序化代码:可以像同步代码一样编写异步逻辑,消除回调链。

二、C++17 协程的实现框架

C++17 并未正式支持协程,但 std::experimental::coroutine 提供了实现原型。主要包含以下组件:

组件 作用
std::experimental::coroutine_handle 控制协程的句柄,负责挂起与恢复
std::experimental::suspend_always / suspend_never 表示挂起行为的协程悬停点
std::experimental::coroutine_traits 为协程函数指定返回类型与 promise 类型
promise_type 协程内部状态管理对象,负责协程的创建、销毁以及异常传播

下面给出一个最小可行的协程示例,演示如何在 C++17 环境下使用 std::experimental

#include <experimental/coroutine>
#include <iostream>
#include <string>

struct Task {
    struct promise_type;
    using handle_type = std::experimental::coroutine_handle <promise_type>;

    struct promise_type {
        Task get_return_object() { return {handle_type::from_promise(*this)}; }
        std::experimental::suspend_always initial_suspend() { return {}; }
        std::experimental::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

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

Task asyncPrint(const std::string& msg) {
    std::cout << msg << std::endl;
    co_return;
}

int main() {
    auto t = asyncPrint("Hello, coroutine!");
    t.coro.resume();   // 开始执行
}

上述代码演示了如何定义 Task 协程类型、提供 promise_type 并在协程内部使用 co_return。值得注意的是:

  • initial_suspendfinal_suspend 可以控制协程是否立即挂起或在完成后挂起。
  • co_return 与普通函数的 return 语义相同。

三、典型使用场景

1. 异步 I/O

在网络编程中,协程可以让 I/O 操作像同步代码一样写,避免回调链。示例伪代码:

Task asyncRead(Socket& s) {
    std::string data;
    while (true) {
        co_await s.asyncReadInto(data);  // 该函数返回一个 awaitable 对象
        process(data);
    }
}

2. 生成器(Generator)

协程天然适合作为生成器,支持 co_yield。例如:

struct IntGenerator {
    struct promise_type {
        int value_;
        IntGenerator get_return_object() { return {handle_type::from_promise(*this)}; }
        std::experimental::suspend_always initial_suspend() { return {}; }
        std::experimental::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
        std::experimental::suspend_always yield_value(int v) {
            value_ = v; return {};
        }
    };
    using handle_type = std::experimental::coroutine_handle <promise_type>;
    handle_type coro;
};

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

使用时:

auto gen = range(1, 5);
while (!gen.coro.done()) {
    std::cout << gen.coro.promise().value_ << ' ';
    gen.coro.resume();
}

3. 状态机

协程内部的挂起点可以用来实现复杂状态机,而无需显式维护状态变量。

四、在 C++17 项目中引入协程的实战技巧

  1. 使用标准库实验扩展
    #include <experimental/coroutine> 并开启实验编译标志(如 -std=c++17 -fcoroutines-std=c++20)。
  2. 封装 awaitable
    为 I/O 或计时器等提供 await_ready, await_suspend, await_resume 接口,便于使用 co_await
  3. 异常安全
    promise_typeunhandled_exception 中统一处理异常,避免协程泄露资源。
  4. 内存管理
    协程句柄使用 destroy() 手动销毁,避免堆栈泄露;也可以使用 std::shared_ptr 包装协程句柄,形成引用计数。

五、总结

尽管 C++17 只提供了实验性的协程实现,但它为 C++ 开发者提供了一条探索更高效、可读的异步编程路径。通过 std::experimental::coroutine,我们可以在 C++17 项目中尝试协程的使用,提前为迁移到 C++20 做好准备。协程让异步代码保持同步写法,显著降低了回调地狱,提高了代码可维护性;同时,协程的轻量级切换也为高并发应用带来了更好的性能表现。未来随着标准化,协程将成为 C++ 生态中不可或缺的一部分。

发表评论