本文接着探讨准备进入C++20的Coroutine。
这篇讲co_await操作。记得前面举了一个例子。一个Coroutine就像是一个巧克力块,可以撕开分成小份分给不同的人吃。co_await操作就像是这个撕开的操作,允许你把函数体撕开,分开执行。co_await提供足够的灵活性,让你能够做一些决定:
- 要撕还是不撕
- 撕开之后还可以想一想,是自己吃还是别人吃
具体而言,co_await awaitable;
这个操作会被编译器改写成:
if (not awaitable.await_ready()) {
// suspend point;
awaitable.await_suspend(coroutine_handle);
// return to the caller
// resume point;
}
awaitable.await_resume();
首先通过await_ready()可以判断要不要撕开函数体,如果不撕,则直接跳去执行await_resume(),当作什么都没有发生。await_resume()可以返回一个值,作为co_await操作的值。如果决定要撕,那么首先会经过suspend point,在这个点上,编译器会把函数执行到现在其栈上的状态保存到动态分配的内存中,为恢复做准备。在这之后会调用await_suspend(),顺便把coroutine_handle传进去。由于执行await_suspend()的时候,相关状态已经保存到动态分配的内存中了,await_suspend()可以跳到另外一个线程上,通过调用coroutine_handle的方法再恢复执行这个coroutine(有点乾坤大挪移的感觉)。这时候这个Coroutine其实就跨了两个线程。在原线程,coroutine会执行到return to the caller
这个点,然后把控制交还调用函数;而在新线程,由于coroutine已经挂起,控制权也转移至coroutine之外。这时候的coroutine是休眠状态,在栈上没有痕迹。唯一能证明其存在的,是原线程中所拥有的coroutine的返回对象,以及新线程中所拥有的coroutine_handle(有没有东食西宿的感觉)。这时候,新线程可以调用coroutine_handle的方法来恢复其执行。这样这个coroutine就会从resume point开始执行。
如果两个线程同时执行了恢复操作,则根据规范 C++ Coroutines TS n4680所说的:Synchronization: a concurrent resumption of the coroutine via resume, operator(), or destroy may result in a data race
上面例子中的await_suspend返回的是void类型,但是它也可以返回其他类型。如果await_suspend返回bool,那么只有其值为true的时候才返回到调用者。await_suspend的另一个可能的返回类型是coroutine_handle,这样的话,这个返回的coroutine_handle会被恢复执行。
总的来说,co_await给了函数体一个机会,可以把自己拆开。有点像一个进程可以fork出另外一个进程的感觉。
不同形式的awaitable
能够被co_await的类型称为awaitable,具有前面列举的await_*成员函数。如果一个类型没有这些个成员函数,其实也没有问题。可以通过一个接受此类型的operator co_await来返回一个能够支持awaitable的类型。还有第三个选项,如果promise_type里面包含了一个可以接受此类型的await_transform()方法,那么则会使用这个await_transform()来返回一个awaitable。
这三个选项的优先次序是这样的:
- promise_type的await_transform()
- operator co_await(),如果存在的话
- co_await的操作对象
小结
至此,C++中coroutine的基本面貌(虽然不是全部面貌)已介绍完毕。Coroutine并不是新鲜的东西,而是早已有之,其他语言中也都存在。比如在C++ Coroutines - Gor Nishanov - CppCon 2015.pdf]中介绍的,下列语言中皆有Coroutine。
Dart1.9
Future<int> getPage(t) async {
var c = new http.Client();
try {
var r = await c.get('http://url/search?q=$t');
print(r);
return r.length();
} finally {
await c.close();
}
}
Python: PEP 0492
async def abinary(n):
if n <= 0:
return 1
l = await abinary(n - 1)
r = await abinary(n - 1)
return l + 1 + r
C#
async Task<string> WaitAsynchronouslyAsync()
{
await Task.Delay(10000);
return "Finished";
}
从某种角度,Coroutine可以看成是一个通用化的函数(Generalized Function)。在传统单入单出的函数(Plain Old Function)之外,增加若干可能性:
- Monadic* (await - suspend)
- Task (await)
- Generator (yield)
- Async Generator(await + yield)
参考
- 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
- conduit
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
(本篇完)