本文接着探讨准备进入C++20的Coroutine。

前面介绍了一个Coroutine:future<void> sweet(),并且说到编译器会根据future<void>::promise_type来对Coroutine的函数体进行调整。这个promise_type大致如下:

template<...>
struct future
{
  struct promise_type
  {
    future get_return_object()
    {
      return future(coroutine_handle<promise_type>::from_promise(this));
    }
    auto initial_suspend() { return suspend_never{}; }
    auto final_suspend() { return suspend_never{}; }
    void return_void() { }
  };
  // ...
};

像上一篇文章所展示的,其中get_return_object()、initial_suspend()、final_suspend()的调用会被插入到编译器调整之后的coroutine函数体中。

promise_type中的return_void()是给co_return操作用的。当执行下面的语句时:

co_return;

会被编译器改写成:

__context->_promise->return_void(); goto final_suspend;

那如果时要返回一个值,比如const char*,该怎么办呢?这时候promise_type要提供一个return_value(const char* string)方法,这样编译器可以把下面的语句:

co_return "hello";

改写成

__context->_promise->return_value("hello"); goto final_suspend;

记得C++中的Coroutine还支持co_yield关键字。这其实和promise_type也有关系。co_yield "hello";会被改写成co_await __context->_promise->yield_value("hello")。也就是说promise_type必须具有yield_value(…)方法,才可以支持co_yield操作。

所以,要成为一个Coroutine,对函数d额返回值是由要求的。这个返回值的类型必须有一个嵌套的子类型promise_type。但是,有时候存在何种情况,一个类型没有嵌套的子类型,却又要把它作为Coroutine函数的返回类型,该怎么办呢?这时候可以使用coroutine_traits模板。如果要将一个Foo类型转化为能够支持Coroutine返回值的类型,则可以以Foo类型来特化coroutine_traits模板,从中定义promise_type。还可以通过特化coroutine_traits<Foo, Args…> ,来让它接收额外的参数。

coroutine_handle

coroutine_handle也是一个模板,coroutine_handle定义如下:

template <> struct coroutine_handle<void>{
  constexpr coroutine_handle() noexcept;
  constexpr coroutine_handle(nullptr_t) noexcept;
  coroutine_handle& operator=(nullptr_t) noexcept;
  constexpr void* address() const noexcept;
  constexpr static coroutine_handle from_address(void* addr);
  constexpr explicit operator bool() const noexcept;
  bool done() const;
  void operator()();
  void resume();
  void destroy();
private:
  void* ptr;// exposition only
};

通常针对每种promise_type,会从coroutine_handle<>派生出相应的针对此种promise_type的特化版的coroutine_handle:

template <typename Promise>
struct coroutine_handle
: coroutine_handle<void>
{
  Promise& promise() const noexcept;
  static coroutine_handle from_promise(Promise&) noexcept;
};

coroutine_handle用于控制coroutine的生命周期。比如,coroutine_handle的resume()用来恢复coroutine的执行;destroy()用来释放用于保存coroutine状态而分配额动态内存;done()用于告知一个coroutine是否已经destoy;operator()()用于coroutine额初次执行。

有两个条件能让coroutine释放,一个是显示调用destroy();另一个是coroutine执行完final_suspend之后自动释放。这里需要注意的是,不能让coroutine释放两次,否则跟free内存两次额效果类似。

好,现在让我们回到promise_type的get_return_object(),可以看到它传了一个coroutine_handle给future的构造函数。随后future可以通过这个传入的coroutine_handle来控制coroutine的执行(比如恢复它):

    future get_return_object()
    {
      return future(coroutine_handle<promise_type>::from_promise(this));
    }

内存分配

当编译器需要为Coroutine动态内存的时候,调用new。如果想自定义内存分配的话,可以在promise_type里面添加new()和get_return_object_on_allocation_failure():

  void* operator new(std::size_t) noexcept {
    return nullptr;   
  }
  static resumable get_return_object_on_allocation_failure(){
    throw std::bad_alloc();   
  }

注意:上文中new()是noexcept的,如果遇到此new()分配失败的情况,编译器会调用get_return_object_on_allocation_failure()来进行处理。

小记

CppCon 2016: Gor Nishanov “C++ Coroutines: Under the covers"中列举了如何利用编译器提供的支持在C语言中实现Coroutine。C++版本的Coroutine原理上和C语言的版本类似,不过使用了一些C++的特性,让编写Coroutine更符合C++的风格。

另外有一点值得注意,Coroutine经常要挂起和恢复,其本地变量的存储位置会发生改变,有时候在栈上,有时候在动态分配的内存里面。取本地变量地址的时候要千万小心。

await/yield: C++ coroutines: Zbigniew Skowron30 November 2016里面讲的例子也很清晰易懂,适合参考学习。

CppCon

Spec

Other

Stackful vs Stackless

(本篇完)

2020-07-10更新

C++ Coroutines in Visual Studio 2019 Version 16.8在MSVC中添加了这一条:

  • Coroutine promise constructor parameters

这意味着Add parameter preview to coroutine promise constructor的提案中的方案被实现了。

更多内容可以参考

(更新完)