初探 C++20 协程

2020.07.25
SF-Zhou

去年学习 libco 时写过一篇博文,讲述如何使用汇编实现协程切换。今年官方的协程实现更加成熟,编译器原生支持加上无栈的设计都吸引着笔者,即使工作中没法用上也想一探究竟。最近 Mac 上可以直接通过 Homebrew 安装 gcc 10.1 了,借此机会探索一下 C++20 协程的玩法。

首先建议阅读参考文献 1,可以搭配本文中的几个例子同步学习。首先看这个例子🌰(在线执行):

#include <coroutine>
#include <iostream>

struct Awaiter {
  bool await_ready() {
    std::cout << "await ready or not" << std::endl;
    return true;
  }

  void await_resume() {
    std::cout << "await resumed" << std::endl;
  }

  void await_suspend(std::coroutine_handle<> h) {
    std::cout << "await suspended" << std::endl;
  }
};

struct Promise {
  struct promise_type {
    auto get_return_object() noexcept {
      std::cout << "get return object" << std::endl;
      return Promise();
    }

    auto initial_suspend() noexcept {
      std::cout << "initial suspend, return never" << std::endl;
      return std::suspend_never{};
    }

    auto final_suspend() noexcept {
      std::cout << "final suspend, return never" << std::endl;
      return std::suspend_never{};
    }

    void unhandled_exception() {
      std::cout << "unhandle exception" << std::endl;
      std::terminate();
    }

    void return_void() {
      std::cout << "return void" << std::endl;
      return;
    }
  };
};

Promise CoroutineFunc() {
  std::cout << "before co_await" << std::endl;
  co_await Awaiter();
  std::cout << "after co_await" << std::endl;
}

int main() {
  std::cout << "main() start" << std::endl;
  CoroutineFunc();
  std::cout << "main() exit" << std::endl;
}

/*
main() start
get return object
initial suspend, return never
before co_await
await ready or not
await resumed
after co_await
return void
final suspend, return never
main() exit
*/

当函数中使用 co_await / co_yield / co_return 关键字时,编译器会将该函数识别为协程。每个协程函数都需要使用协程状态 coroutine state 来存储内部数据,包括协程承诺 promise、传入协程的参数、当前挂起点的某种表示形式以及当前挂起点范围内的局部变量和临时变量。该状态分配在堆上,由编译器负责管理。协程承诺 promise 由协程内部控制,用于协程提交结果或异常;协程句柄 coroutine handle 由协程外部控制,用于恢复协程执行或销毁协程;二者可以通过接口获取到对方。

当调用协程函数时,其步骤如下:

  1. 使用 operator new 申请空间并初始化协程状态;
  2. 复制协程参数到到协程状态中;
  3. 构造协程承诺对象 promise
  4. 调用 promise.get_return_object() 并将其结果存储在局部变量中。该结果将会在协程首次挂起时返回给调用者;
  5. 调用 co_await promise.initial_suspend(),预定义了 std::suspend_always 表示始终挂起,std::suspend_never 表示始终不挂起;
  6. 而后正式开始执行协程函数内过程。

当协程函数执行到 co_return [expr] 语句时:

  1. exprvoid 则执行 promise.return_void(),否则执行 promise.return_value(expr)
  2. 按照创建顺序的倒序销毁局部变量和临时变量;
  3. 执行 co_await promise.final_suspend()

当协程执行到 co_yield expr 语句时:

  1. 执行 co_await promise.yield_value(expr)

当协程执行到 co_await expr 语句时:

  1. 通过 expr 获得 awaiter 对象;
  2. 执行 awaiter.await_ready(),若为 true 则直接返回 awaiter.await_resume()
  3. 否则将协程挂起并保存状态,执行 awaiter.await_suspend(),若其返回值为 void 或者 true 则成功挂起,将控制权返还给调用者 / 恢复者;
  4. 直到 handle.resume() 执行后该协程才会恢复执行,将 awaiter.await_resume() 作为表达式的返回值。

当协程因为某个未捕获的异常导致终止时:

  1. 捕获异常并调用 promise.unhandled_exception()
  2. 调用 co_await promise.final_suspend()

当协程状态销毁时(通过协程句柄主动销毁 / co_return 返回 / 未捕获异常):

  1. 析构 promise 对象;
  2. 析构传入的参数;
  3. 回收协程状态内存。

简明来看,一个协程函数会被编译器执行类似下方的展开:

// 协程函数
template <typename R, typename... Args>
R Func(Args... args) {
  auto ret = co_await Awaiter();
  co_yield ret;
  co_return;
}

// 编译器展开
template <typename R, typename... Args>
R Func(Args... args) {
  using promise_t = typename coroutine_traits<R, Args...>::promise_type;

  promise_t promise;  // 实际上分配在堆上而非栈上,这里为了方便说明简化了
  auto __return__ = promise.get_return_object();

  co_await promise.initial_suspend();

  try {
    // auto ret = co_await Awaiter();
    auto &&value = Awaiter();
    auto &&awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
    auto &&awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));
    if (!awaiter.await_ready()) {
      using handle_t = std::coroutine_handle<Promise>;
      using await_suspend_result_t =
          decltype(awaiter.await_suspend(handle_t::from_promise(promise)));
      // 协程挂起
      if constexpr (std::is_void_v<await_suspend_result_t>) {
        awaiter.await_suspend(handle_t::from_promise(promise));
        return __return__;  // 返还控制权给调用者/恢复者
      } else {
        static_assert(std::is_same_v<await_suspend_result_t, bool>,
                      "await_suspend() must return 'void' or 'bool'.");
        if (awaiter.await_suspend(handle_t::from_promise(promise))) {
          return __return__;  // 返还控制权给调用者/恢复者
        }
      }
      // 协程恢复点,handle.resume() 后在此处恢复
    }
    auto ret = awaiter.await_resume();  // 返回 resume 的值

    // co_yield ret;
    co_await promise.yield_value(ret);

    // co_return;
    promise.return_void();
    goto final_suspend;
  } catch (...) {
    promise.set_exception(std::current_exception());
  }

final_suspend:
  co_await promise.final_suspend();
}

第一个样例中 await_ready() 始终返回 true,协程并不会挂起而是继续同步执行直到结束。看另一个例子🌰(在线执行):

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

std::coroutine_handle<> handle;

struct Awaiter {
  bool await_ready() {
    std::cout << "await ready or not" << std::endl;
    return false;
  }

  void await_resume() {
    std::cout << "await resumed" << std::endl;
  }

  void await_suspend(std::coroutine_handle<> h) {
    std::cout << "await suspended" << std::endl;
    handle = h;
  }
};

struct Promise {
  struct promise_type {
    auto get_return_object() noexcept {
      std::cout << "get return object" << std::endl;
      return Promise();
    }

    auto initial_suspend() noexcept {
      std::cout << "initial suspend, return never" << std::endl;
      return std::suspend_never{};
    }

    auto final_suspend() noexcept {
      std::cout << "final suspend, return never" << std::endl;
      return std::suspend_never{};
    }

    void unhandled_exception() {
      std::cout << "unhandle exception" << std::endl;
      std::terminate();
    }

    void return_void() {
      std::cout << "return void" << std::endl;
      return;
    }
  };
};

Promise CoroutineFunc() {
  std::cout << "before co_await" << std::endl;
  co_await Awaiter();
  std::cout << "after co_await" << std::endl;
}

int main() {
  std::cout << "main() start" << std::endl;
  CoroutineFunc();

  std::this_thread::sleep_for(std::chrono::seconds(1));
  std::cout << "resume coroutine after one second" << std::endl;
  handle.resume();

  std::cout << "main() exit" << std::endl;
}

/*
main() start
get return object
initial suspend, return never
before co_await
await ready or not
await suspended
resume coroutine after one second
await resumed
after co_await
return void
final suspend, return never
main() exit
*/

这里 awaiter.await_ready() 返回 false,协程会被挂起。之后 awaiter.await_suspend(handle) 被执行,handle 也就是上文提到的协程句柄。该函数需要完成对句柄的调度,以便之后由执行器去恢复该协程或将其销毁。若 awaiter.await_suspend(handle) 的返回类型为 voidco_await 会立即将执行权归还给调用者。直到 handle.resume() 执行时,协程才会恢复执行,并且会调用 awaiter.await_resume() 并将其返回值作为 co_await awaiter 的返回值。上面的例子中将协程句柄赋值给了全局变量 handle,而后协程挂起,回到 main 函数,睡眠 1s 后调用 handle.resume() 恢复。

awaiter.await_suspend(handle) 执行前协程已完成挂起,此时协程状态中已经保存了恢复所需要的各种状态,所以也可以将协程句柄丢到另一个线程中恢复执行,再看一个例子🌰(在线执行):

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

std::jthread thread;

struct Awaiter {
  Awaiter() {
    std::cout << "Awaiter()" << std::endl;
  }

  ~Awaiter() {
    std::cout << "~Awaiter()" << std::endl;
  }

  bool await_ready() {
    std::cout << "await ready or not" << std::endl;
    return false;
  }

  void await_resume() {
    std::cout << "await resumed" << std::endl;
  }

  void await_suspend(std::coroutine_handle<> h) {
    std::cout << "await suspended" << std::endl;

    thread = std::jthread([h] {
      std::this_thread::sleep_for(std::chrono::seconds(1));
      std::cout << "resume coroutine in another thread" << std::endl;
      h.resume();
    });
  }
};

struct Promise {
  struct promise_type {
    auto get_return_object() noexcept {
      std::cout << "get return object" << std::endl;
      return Promise();
    }

    auto initial_suspend() noexcept {
      std::cout << "initial suspend, return never" << std::endl;
      return std::suspend_never{};
    }

    auto final_suspend() noexcept {
      std::cout << "final suspend, return never" << std::endl;
      return std::suspend_never{};
    }

    void unhandled_exception() {
      std::cout << "unhandle exception" << std::endl;
      std::terminate();
    }

    void return_void() {
      std::cout << "return void" << std::endl;
      return;
    }
  };
};

Promise CoroutineFunc() {
  std::cout << "before co_await" << std::endl;
  co_await Awaiter();
  std::cout << "after co_await" << std::endl;
}

int main() {
  std::cout << "main() start" << std::endl;
  CoroutineFunc();
  std::cout << "main() exit" << std::endl;
}

/*
main() start
get return object
initial suspend, return never
before co_await
Awaiter()
await ready or not
await suspended
main() exit
resume coroutine in another thread
await resumed
~Awaiter()
after co_await
return void
final suspend, return never
*/

这里在 awaiter.await_suspend(handle) 中新建了一个 std::jthread,计划在线程中睡眠 +1s 后恢复协程。而后协程挂起,随后 main 函数结束,全局变量析构,等待线程 join() 。于是 1s 后线程等待结束并恢复线程。注意 awaiter 对象在协程恢复后自动析构了。

这几个简单的例子就可以激发大量想象力了。无栈协程加上编译器加持,以前用魔法才能完成的事情现在可以在官方认证下轻松完成。当然目前标准中只提供了协程的基础能力,剩下调度器/执行器、IO 多路复用等都需要使用者自行实现,期待开源社区出现完整易用的异步框架,目前观察到 GitHub 上已经有 folly/corolibcopp 在进行相关工作了。

References

  1. "Coroutines (C++20)", C++ Reference
  2. "Working Draft, C++ Extensions for Coroutines", Open Standards
  3. "C++ Coroutine 简明教程", wpcockroach
  4. "C++ Coroutines: Understanding operator co_await", lewissbaker