Modern C++ Coroutine简介

引言

C++20 的发布标志着协程(coroutines)首次被纳入标准语言. 这一功能通过对异步编程的支持和生成器模式的实现, 极大地改变了程序员处理复杂控制流的方式. 本文将详细介绍 C++20 协程的核心概念, 实现原理, 用法示例以及它在实际应用中的潜力.

C++ 协程(coroutine) 是一种高级控制流工具, 旨在简化异步编程和并发操作. 它通过将函数的执行状态保存并恢复, 允许程序从挂起的位置继续执行, 从而解决了一些传统方法难以优雅处理的问题. 以下是协程解决的主要问题:


1. 简化异步编程

问题:

传统的异步编程需要使用回调(callbacks), 状态机或复杂的线程管理. 这样代码会变得难以理解和维护, 特别是在处理多层嵌套的回调时, 容易导致 回调地狱(callback hell).

协程的解决方式:

协程允许以同步风格编写异步代码. 例如:

示例:

std::future<void> downloadFile() {
    auto data = co_await asyncDownload();
    process(data);
    co_return;
}

2. 高效的并发控制

问题:

传统的线程模型可能因线程切换的开销过大而导致性能问题, 特别是在 I/O 密集型任务中. 使用线程池可以缓解这个问题, 但增加了复杂性.

协程的解决方式:

协程是"协作式多任务"(cooperative multitasking), 没有线程切换的上下文开销, 提供了轻量级的并发支持. 多个协程可以共享同一个线程, 从而显著提升资源利用率.


3. 简化生成器(Generators)和数据流处理

问题:

生成器和数据流处理中需要保存中间状态, 传统实现需要额外管理状态机或全局变量.

协程的解决方式:

协程通过 co_yield 自动管理状态, 可以轻松实现生成器模式, 用于惰性生成数据流.

示例:

// generator 是一个自定义的模板类
generator<int> numbers() {
    for (int i = 0; i < 10; ++i) {
        co_yield i;
    }
}

调用 numbers() 会逐步生成数据, 而无需手动管理迭代器状态.


适用场景

  1. 网络编程: 高性能服务器需要同时处理大量异步请求, 协程使得代码逻辑清晰且高效.
  2. 游戏开发: 游戏中的物理引擎, AI 行为等需要频繁的状态切换, 协程简化了这些逻辑.
  3. 数据流处理: 协程的生成器模式非常适合逐步处理大数据集, 如大规模日志处理和流式计算.
  4. 并发任务管理: 如多任务调度器或事件循环.

C++ 协程

C++20 为协程提供了语言级支持, 通过以下三个关键字实现核心功能:


协程的运行机制

C++20 的协程支持依赖于编译器生成的协程框架, 这一框架包含:

  1. 协程句柄 (Coroutine Handle)

    • std::coroutine_handle 是核心组件, 用于管理协程的生命周期.
    • 它可以启动, 暂停, 恢复和销毁协程.
  2. 协程状态 (Coroutine State)

    • 协程状态存储在协程帧 (coroutine frame) 中, 包含局部变量, 程序计数器等信息.
    • 每个协程帧由编译器分配, 允许协程挂起时保持状态.
  3. 协程特性 (Coroutine Traits)

    • 标准库的 std::coroutine_traits 用于决定协程的返回类型.
    • 开发者可以通过特化 std::coroutine_traits 来定制协程的行为.

C++20 协程的用法示例

示例 1. coroutine 执行流程

定义一个协程

Task GetTask() {
  std::println("One!");
  co_await std::suspend_always{};  // 协程在此处第一次挂起

  std::println("Two!");
  co_await std::suspend_always{};  // 协程在此处第二次挂起

  std::println("Three!");
}

GetTask定义了一个协程, 直观的看这个函数体将会输出三个语句, 只不过期间会有暂停, co_await 将会挂起操作直到resume()方法被调用.

这个函数没有return语句却有返回值:Task. 这是协程的特性, 我们需要一些接口和胶水代码来与协程交互.

调用协程

int main() {
  Task task = GetTask();
  std::println("coroutine GetTask() started");

  while (task.resume()) {
    using namespace std::literals;  // 为了使用 ms 后缀
    std::this_thread::sleep_for(500ms);
  }
  return 0;
}

完整代码

#include <chrono>
#include <coroutine>
#include <print>
#include <thread>

// Promise 模板类, 用于管理协程的状态和生命周期
template <typename T>
struct Promise {
  // 获取返回对象的方法
  auto get_return_object() {
    std::println("Promise::get_return_object()");
    // 从当前 promise 创建一个协程句柄
    return std::coroutine_handle<Promise<T>>::from_promise(*this);
  }

  // 初始挂起点, 在协程开始时调用
  auto initial_suspend() {
    std::println("Promise::initial_suspend()");
    // 返回一个总是挂起的挂起点
    return std::suspend_always{};
  }

  // 最终挂起点, 在协程结束时调用
  auto final_suspend() noexcept {
    std::println("Promise::final_suspend()");
    // 返回一个总是挂起的挂起点
    return std::suspend_always{};
  }

  // 处理未捕获的异常
  void unhandled_exception() { std::terminate(); }

  // 返回 void 类型的方法
  void return_void() { std::println("Promise::return_void()"); }
};

// Task 类, 表示一个协程任务
class Task {
 public:
  // 定义 promise_type 为 Promise<Task>
  using promise_type = Promise<Task>;

  // 构造函数, 接受一个协程句柄
  Task(auto h) : handle_{h} { std::println("Task::construct"); }

  // 析构函数, 销毁协程句柄
  ~Task() {
    std::println("Task::destruct");
    if (handle_) {
      handle_.destroy();
    }
  }

  // 恢复协程的方法
  bool resume() const {
    std::println("Task::resume()");
    if (!handle_) {
      return false;
    }
    handle_.resume();
    return !handle_.done();
  }

 private:
  // 协程句柄, 管理协程的执行
  std::coroutine_handle<promise_type> handle_;
};

// GetTask 函数定义了一个协程任务
// 协程在调用 GetTask() 时创建, 并在 co_await 处挂起
Task GetTask() {
  std::println("One!");
  co_await std::suspend_always{};  // 协程在此处第一次挂起

  std::println("Two!");
  co_await std::suspend_always{};  // 协程在此处第二次挂起

  std::println("Three!");
}

int main() {
  Task task = GetTask();
  std::println("coroutine GetTask() started");

  while (task.resume()) {
    using namespace std::literals;  // 为了使用 ms 后缀
    std::this_thread::sleep_for(500ms);
  }
  return 0;
}

在 Compiler Explorer 中查看

运行输出信息:

Promise::get_return_object()
Promise::initial_suspend()
Task::construct
coroutine GetTask() started
Task::resume()
One!
Task::resume()
Two!
Task::resume()
Three!
Promise::return_void()
Promise::final_suspend()
Task::destruct

示例 2. generator

#include <coroutine>
#include <exception>
#include <iostream>
#include <print>
#include <thread>
#include <vector>

// Generator 类, 表示一个生成器协程
class Generator {
 public:
  // promise_type 结构体, 管理协程的状态和生命周期
  struct promise_type {
    int value_ = 0;  // 当前值

    // yield_value 方法, 用于挂起协程并保存当前值
    auto yield_value(int value) {
      value_ = value;
      return std::suspend_always{};  // 总是挂起协程
    }

    // get_return_object 方法, 返回协程句柄
    auto get_return_object() {
      return std::coroutine_handle<promise_type>::from_promise(*this);
    }

    // initial_suspend 方法, 初始挂起点
    auto initial_suspend() { return std::suspend_always{}; }

    // final_suspend 方法, 最终挂起点
    auto final_suspend() noexcept { return std::suspend_always{}; }

    // unhandled_exception 方法, 处理未捕获的异常
    void unhandled_exception() { std::terminate(); }

    // return_void 方法, 协程返回 void 类型
    void return_void() {}
  };

 public:
  // 构造函数, 接受一个协程句柄
  Generator(auto h) : handle_{h} {}

  // 析构函数, 销毁协程句柄
  ~Generator() {
    if (handle_) {
      handle_.destroy();
    }
  }

  // 恢复协程的方法
  bool resume() const {
    if (!handle_) {
      return false;
    }

    handle_.resume();
    return !handle_.done();
  }

  // 获取当前值的方法
  int getValue() const { return handle_.promise().value_; }

 private:
  // 协程句柄, 管理协程的执行
  std::coroutine_handle<promise_type> handle_;
};

template <typename T>
Generator Visit(const T& coll) {
  for (int elem : coll) {
    std::println("\tyield {}", elem);
    co_yield elem;
    std::println("\tresume");
  }
}

int main() {
  using namespace std::literals;

  std::vector<int> coll{0, 8, 15, 33, 42, 77};
  Generator gen = Visit(coll);

  std::println("start loop:");
  while (gen.resume()) {
    std::println("main(): value: {}", gen.getValue());
    std::this_thread::sleep_for(1s);
  }
}

在 Compiler Explorer 中查看

输出:

start loop:
        yield 0
main(): value: 0
        resume
        yield 8
main(): value: 8
        resume
        yield 15
main(): value: 15
        resume
        yield 33
main(): value: 33
        resume
        yield 42
main(): value: 42
        resume
        yield 77
main(): value: 77
        resume

协程的优缺点

优点

  1. 可读性: 使异步代码的逻辑更加线性, 避免嵌套的回调地狱.
  2. 性能: 通过避免线程切换和不必要的上下文切换, 提升效率.
  3. 灵活性: 适用于多种异步任务, 例如 I/O, 网络请求等.

缺点

  1. 复杂性: 需要深入理解协程框架和生命周期.
  2. 资源管理: 协程暂停时占用内存, 可能导致内存泄漏.
  3. 兼容性: 现有的库和工具对协程支持有限, 需要手动整合.

实践建议

结论

C++20 协程的引入让异步编程变得更加简单和高效. 无论是高性能服务器还是游戏开发, 协程都提供了一种直观且强大的解决方案. 通过熟练掌握协程的核心机制, 开发者可以显著提升代码的可维护性和运行效率.

源码链接

源码链接