本文接着探讨准备进入C++20的Coroutine。
如何理解Coroutine
上一篇文章提到,Coroutine和普通函数有所不同,可以多次挂起和恢复,所以需要用和对待普通函数不同的眼光来看待Coroutine。普通函数单入单出,如果把普通函数比作一个巧克力球,那么这个巧克力球就是那种可以一口吃掉的巧克力球。而Coroutine则,则可以看成那种可以分为小份的巧克力块,可以每次支持一个小份。成功理解这点是理解Coroutine的关键。分成多份的巧克力块可以给同一个人分多次吃,这其实相当于Coroutine中类似Generator的应用。如果把巧克力块其中的某些份分给其他人吃,这就相当于把函数的不同部分分给不同的线程来运行。当然,写Coroutine的人可以决定巧克力块的食用顺序。有的人会觉得好吃的巧克力为什么要分给别人吃?如果吃巧克力是一种工作的话,那么找人分担是不是一种更好的做法呢?
简而言之,可以把Coroutine当成是多个函数拼接在一起所生成的一种新的形态,就像乐高积木那样。
如何让普通函数成为Coroutine
那么在C++中,如何让普通的函数成为Coroutine呢?很简单,只要使用到了co_await、co_yeild、co_return这三个关键字中的一个,这个函数就会被编译器当作Coroutine。
来看下面这一个例子:
future<void> sweet() {
cout << "header" << endl;
co_await suspend_always{};
cout << "middle" << end;
co_await suspend_always{};
cout << "footer" << endl;
}
上面示意的例子中,用到了两个co_await suspend_always{}
语句,每个语句其实创建了一个Coroutine的挂起点。执行co_await suspend_always{}
之后,该Coroutine被挂起而停止执行,直到被恢复为止。而两个co_await suspend_always{}
把该Coroutine分成了三份,执行后分别为输出header、middle以及footer。
suspend_always目前定义在std::experimental空间,用于co_await时会挂起coroutine。它还有个兄弟叫做suspend_never,执行co_await的时候不会挂起,相当于没有效果,在特殊的情境下有用。
如果一个函数时Coroutine,编译器会对其添油加醋,上面的sweet()
例子会被扩展成:
future<void> sweet()
{
__sweet_context* __context = new __sweet_context{};
__return = __context->_promise.get_return_object();
co_await __context->_promise.initial_suspend();
cout << "header" << endl;
co_await suspend_always{};
cout << "middle" << end;
co_await suspend_always{};
cout << "footer" << endl;
__final_suspend_label:
co_await __context->_promise.final_suspend();
}
上面的例子参考自Introduction to C++ Coroutines Slides。其中__sweet_context时编译器生成的一个上下文,用于保存coroutine挂起还原时所需要的动态空间(如果不需要这个空间,编译器完全可以把这个分配操作优化掉)。sweet()的返回类型是future<void>
,但实际上,该返回类型是通过__return = __context->_promise.get_return_object();
创建的__return对象。
在正式执行coroutine之前,会先执行 co_await __context->_promise.initial_suspend();
来判断是否要一开始就挂起coroutine,如果不需要,那么initial_suspend()可以返回suspend_never对象。在coroutine结束之前,则会执行co_await __context->_promise.final_suspend();
,看是否需要结束前挂起。总之,编译器对coroutine的拆分程度,比你想象得高。
initial_suspend和final_suspend都来自__context->_promise
,那么_promise是什么类型呢?其实_promise的类型来自于future<void>
。编译器会去futuer<void>
下搜寻是否有struct promise_type,如果有,则把它当作_promise的类型。
欲知struct promise_type的具体内容,请听下回分解。
- 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"
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
(本篇完)