本文接着探讨准备进入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里面讲的例子也很清晰易懂,适合参考学习。
- COROUTINES INTRODUCTION
- YOUR FIRST COROUTINE
- CO_AWAITING COROUTINES
- Coroutine Theory
- C++ Coroutines: Understanding the promise type
- C++ Coroutines: Understanding operator co_await
- Writing your own C++ coroutines: getting started
- C++ coroutine tutorial - computing fibonacci using C++ coroutines
- cppcoro
CppCon
- CppCon 2016: James McNellis “Introduction to C++ Coroutines"
- Introduction to C++ Coroutines Slides
- CppCon 2016: Kenny Kerr and James McNellis “Putting Coroutines to Work with the Windows Runtime”
- Putting Coroutines to Work with the Windows Runtime Slides
- CppCon 2016: Gor Nishanov “C++ Coroutines: Under the covers"
- Embracing Standard C++ for the Windows Runtime Slides
- C++ Coroutines - Gor Nishanov - CppCon 2015.pdf
- CppCon 2015: Gor Nishanov “C++ Coroutines - a negative overhead abstraction"
- CppCon 2017: Toby Allsopp “Coroutines: what can’t they do?”
Spec
- C++ Coroutines TS n4740
- C++ Coroutines TS n4680
- n4755 - open-std
- P0978R0 - open-std
- CPP References: Coroutines (C++20)
Other
- Beginning the coroutine with Visual Studio 2015 Update 3 Part 1
- Beginning the coroutine with Visual Studio 2015 Update 3 Part 2
- How C++ coroutines work
- Ranges, Coroutines, and React: Early Musings on the Future of Async in C++
- Is the Coroutines TS over-engineered?
- What are coroutines in C++20?
- Coroutines
- C++ coroutines / Visual Studio: How can a generator call a function that yields values on its behalf?
- Variadic generators in C++
- Coroutine Types
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的提案中的方案被实现了。
更多内容可以参考
- C++ coroutines: Promise constructors
- C++ coroutines: Constructible awaitable or function returning awaitable?
- https://github.com/lewissbaker/cppcoro
(更新完)