

The Coroutine in C++ 20 协程初探
source link: https://zhuanlan.zhihu.com/p/237952115
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

The Coroutine in C++ 20 协程初探
C++已经从C++11演化到了C++20,你还在用C++98吗?
本文将唠唠C++ 20的coroutine(协程),如果对coroutine,async, await不了解的,可以先移步阅读Coroutine, 异步,同步,async, await——
Coroutine就是函数,只不过是可以suspend和resume的函数,也就是你可以暂停这个函数的执行(实际上就是在suspend的地方直接返回到caller了),去做其他事情,然后在恰当的时候恢复到你离开的位置继续开始运行。
如下图左边灰色的coroutine(特别点的函数),当它被调用的时候,被切分成了两个部分,在线程1执行完成第一部分后,就继续做其他事情了,第二部分被suspend了。在适当的时机出现后,线程2(可以是线程1)开始执行第二部分。

把一个完整的函数切分成多个部分,有什么好处呢?请看Coroutine, 异步,同步,async, await。本文关注C++是如何实现这样的效果~
JavaScript里面的coroutine使用起来很方便,长这个样子
async reply() {
get_name(); //normal function call
data = await read_data();
res = await write(data);
return res;
}
它们长得跟同步的代码很像,只是在block的函数调用前面添加了await关键字,而函数本身reply()标注了async关键字。它们实际执行的逻辑顺序不变(也就是按照代码书写的顺序执行)但是却是异步执行的。具体可以参见Coroutine, 异步,同步,async, await。
C++的coroutine长什么样子呢?
Coroutine in C++
C++虽然是接近底层的编程语言,但C++ coroutine代码长得也差不多,如下
sync<int> reply() {
get_name(); // normal function call
std::string data = co_await read_data();
int res = co_await write(data);
return res;
}
可是要让上面的代码成功编译,不是一件简单的事情。这里先抛出能让它编译成功的完整的代码,可以暂时忽略具体的细节,感兴趣可以用VS2015/2019, Clang/GCC 10.0去尝试编译一下。发现太长了,我把它单独放到这里:A Simple C++ Coroutine
代码我加了Trace,方便跟踪程序的执行顺序。如果点进去随便瞄一下,会发现代码很长,第一次接触肯定一脸o((⊙﹏⊙))o 。下面具体分解。
首先reply()函数就叫coroutine,也就是它可以suspend,然后resume。那什么支持了它这么灵活呢?
大体上两个方面,一个编译器的支持,也就是完整的代码(指这里的代码:A Simple C++ Coroutine,下文意同)需要比较新的C++编译器才能编译;另一个方面是程序员按照跟编译器的约定编写特定的代码。
把函数变成Coroutine
将本文要讲解的代码从完整的代码摘出来如下,可以暂时略过:
template<typename T>
class lazy {
bool await_ready()
{
const auto ready = this->coro.done();
Trace t;
std::cout << "Await " << (ready ? "is ready" : "isn't ready") << std::endl;
return this->coro.done();
}
void await_suspend(std::experimental::coroutine_handle<> awaiting)
{
{
Trace t;
std::cout << "About to resume the lazy" << std::endl;
this->coro.resume();
}
Trace t;
std::cout << "About to resume the awaiter" << std::endl;
awaiting.resume();
}
auto await_resume()
{
const auto r = this->coro.promise().value;
Trace t;
std::cout << "Await value is returned: " << r << std::endl;
return r;
}
}
lazy<std::string> read_data()
{
Trace t;
std::cout << "Reading data..." << std::endl;
co_return "billion$!";
}
lazy<int> write_data()
{
Trace t;
std::cout << "Write data..." << std::endl;
co_return 42;
}
sync<int> reply(int i)
{
std::cout << "Started await_answer" << std::endl;
auto a = co_await read_data();
std::cout << "Data we got is " << a << std::endl;
auto v = co_await write_data();
std::cout << "write result is " << v << std::endl;
co_return 42;
}
因为reply()函数里面有co_await,要包装成coroutine,所以我们要sync<int>支持特定的接口,具体介绍放在单独的文章:The Coroutine in C++ 20 协程之诺。
而read_data()和write_data()被co_await了,我们需要它们的返回的lazy<std::string>支持下面三个接口
- bool await_read()
- auto await_suspend(HandleType handle)
- auto await_resume()
其中 using HandleType = std::experimental::coroutine_handle<>
有了这三个接口,当reply() coroutine 进行co_await需要suspend的时候,await_suspend就被调用;当函数resume的时候,await_resume()就会被调用。
而函数什么时候会被suspend呢?当await_ready()返回false的时候,所以程序员需要在await_ready里面定义该不该suspend, 如:
bool await_ready()
{
const auto ready = this->coro.done();
Trace t;
std::cout << "Await " << (ready ? "is ready" : "isn't ready") << std::endl;
return this->coro.done();
}
当把函数suspend的时候,什么时候reply()会被resume呢?程序员需要在await_suspend()里面编写相应的调度代码。比如完整的代码里面,我们就马上resume了:
void await_suspend(std::experimental::coroutine_handle<> awaiting)
{
//...省略部分代码
Trace t;
std::cout << "About to resume the reply()" << std::endl;
awaiting.resume();
}
实际的产品的代码会将coroutine (reply())放到到事件调度器,比如libuv(NodeJS的时间循环,我正在利用C++ 20 Coroutine使得libuv支持co_await,后续会讲解如何实现)。
而怎么resume呢?很简单,await_suspend()的传入参数就是一个reply() coroutine的handle,我们直接handle.resume()就将reply() resume了,见上面的代码。
是不是很神奇?
所以这三个接口的功能是:
1 await_ready()示意被co_await 的对象(read_data(),write_data())要不要将当前的coroutine (reply) suspend(挂起)。
2 await_suspend()负责当挂起的时候,定义什么时候可以被resume。
3 await_resume()定义当被 co_await的对象resume时要将什么作为co_await的返回值。
为什么会这么”神奇“
完整的代码read_data()和write_data()返回的lazy<std::string>还需要一些工作,比如initial_suspend, final_suspend,get_return_object等等,The Coroutine in C++ 20 协程之诺将继续讲解~
Coroutine关键词有await, yield, async。在JavaScript里面一般不叫coroutine,叫async call或者Promise。
C++ 对应有co_await,co_yield, 还多了个co_return。async对应什么呢?请看我的续篇The Coroutine in C++ 20 协程之诺
参考文章:
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK