本文接着探讨准备进入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。

这三个选项的优先次序是这样的:

  1. promise_type的await_transform()
  2. operator co_await(),如果存在的话
  3. 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)

参考

CppCon

Spec

Other

Stackful vs Stackless

(本篇完)