
导语 | 本文推选自腾讯云开发者社区-【技思广益 · 腾讯技术人原创集】专栏。该专栏是腾讯云开发者社区为腾讯技术人与广泛开发者打造的分享交流窗口。栏目邀约腾讯技术人分享原创的技术积淀,与广泛开发者互启迪共成长。本文作者是腾讯后台开发工程师杨良聪。
协程(coroutine)是在执行过程中可以被挂起,在后续可以被恢复执行的函数。在C++20中,当一个函数内部出现了co_await、co_yield、co_return中的任何一个时,这个函数就是一个协程。

C++20协程的一个简单的示例代码:
coro_ret<int> number_generator(int begin, int count) { std::cout << "number_generator invoked." << std::endl; for (int i=begin; i<count; ++i) { co_yield i; } co_return;}
int main(int argc, char* argv[]){ auto g = number_generator(1, 10); std::cout << "begin to run!" << std::endl; while(!g.resume()) { std::cout << "got number:" << g.get() << std::endl; }
std::cout << "coroutine done, return value:" << g.get() << std::endl;
return 0;}number_generator内出现了co_yield和co_return所以这不是一个普通的函数,而是一个协程,每当程序执行到第4行co_yield i;时,协程就会挂起,程序的控制权会回到调用者那里,直到调用者调用resume方法,此时会恢复到上次协程yield的地方,继续开始执行。

number_generator的返回类型是coro_ret<int>,而协程本身的代码中并没有通过return返回这个类型的数据,这就是C++20里实现协程的一个关键点: 协程的返回类型T中,必须有T::promise_type这个类型定义,这个类型要实现几个接口。还是先看代码:
//!coro_ret 协程函数的返回值,内部定义promise_type,承诺对象template <typename T>struct coro_ret{ struct promise_type; using handle_type = std::coroutine_handle<promise_type>; //! 协程句柄 handle_type coro_handle_; //!promise_type就是承诺对象,承诺对象用于协程内外交流 struct promise_type { promise_type() { std::cout << "promise constructor invoded." << std::endl; } ~promise_type() = default;
//!生成协程返回值 auto get_return_object(){ std::cout << "get_return_object invoked." << std::endl; return coro_ret<T>{handle_type::from_promise(*this)}; }
//! 注意这个函数,返回的就是awaiter //! 如果返回std::suspend_never{},就不挂起, //! 返回std::suspend_always{} 挂起 //! 当然你也可以返回其他awaiter auto initial_suspend(){ //return std::suspend_never{}; std::cout << "initial_suspend invoked." << std::endl; return std::suspend_always{}; } //!co_return 后这个函数会被调用 /* void return_value(const T& v) { return_data_ = v; return; } */
void return_void(){ std::cout << "return void invoked." << std::endl; }
//! auto yield_value(const T& v){ std::cout << "yield_value invoked." << std::endl; return_data_ = v; return std::suspend_always{}; //return std::suspend_never{}; } //! 在协程最后退出后调用的接口。 auto final_suspend() noexcept{ std::cout << "final_suspend invoked." << std::endl; return std::suspend_always{}; } // void unhandled_exception(){ std::cout << "unhandled_exception invoked." << std::endl; std::exit(1); } //返回值 T return_data_; };
coro_ret(handle_type h) : coro_handle_(h) { } ~coro_ret() { //!自行销毁 if (coro_handle_) { coro_handle_.destroy(); } }
//!恢复协程,返回是否结束 bool resume(){ if (!coro_handle_.done()) { //! 如果已经done了,再调用resume,会导致coredump coro_handle_.resume(); } return coro_handle_.done(); }
bool done() const{ return coro_handle_.done(); }
//!通过promise获取数据,返回值 T get(){ return coro_handle_.promise().return_data_; }
};coro_ret是个自定义的结构,为了能作为协程的返回值,需要定义一个promise_type。这个类型需要实现如下的接口:

可以看出promise类的工作主要是两个:一是定义协程的执行流程,主要接口是initial_suspend,final_suspend,二是负责协程和调用者之间的数据传递,主要接口是yield_value和return_value。
std::coroutine_handle<promise_type>是协程的控制句柄类,最重要的接口是promise、resume,前者可以获得协程的promise对象,后者可以恢复协程的运行。此外还有destroy接口,用来销毁协程实例,done接口用于返回协程是否已经结束运行。通过std::coroutine_handle<promise_type>::from_promise()方法,可以从promise实例获得对应的handle。
coro_ret中其他几个接口resume,done和get_data不是必须的,只是为了方便使用而存在。
总结一下,一个协程与这几个对象关联在一起:
这是个在堆上分配的内部对象,没有暴露给开发者,是用来保存协程内相关数据和状态的,具体来说就是:



要在c++20里实现一个协程,需要定义一个协程的返回类型T,这个T内需要定义一个promise_type的类型,这个类型要实现几个指定的接口,这样就足够了。这样,要开发一个包含异步操作的协程,代码的结构大致会是这样的:
coro_return<T> logic() { // 发起异步操作 some_async_oper(); co_yield xxx // 恢复执行了,要先检查和获得异步操作的结果 auto result = get_async_oper_result() do_some_thing(result)
co_return}
int main() { auto co_ret = logic(); // 循环检查异步操作是否结束 while(true) { auto result = get_async_result(); if (result) { // 异步操作结束了,恢复协程的运行,要把结果传过去 co_ret.resume() break; } }}可以看到,在协程内部,发起异步操作和获取结果,被yield分割为了两步,和同步代码还是有着明显的区别。这时,co_await就可以发挥它的作用了,使用了co_await后的协程代码会是这样的
coro_return<T> logic() { auto result = co_await some_async_oper(); do_some_thing(result);}这样就和同步代码就基本没有区别了,除了这个co_await
co_await最常见的使用方式为auto ret=co_await expr,co_await后跟一个表达式,整个语句的执行过程有多种情况,是比较复杂的。这里描述的是简化版本,主要是简化了promise.await_transform的作用,以及awaitable对象,可以点击下面链接看完整的描述。这里假定协程的promise_type没有实现await_transform方法。
https://en.cppreference.com/w/cpp/language/coroutines

用代码表达,是这样:
if (!awaiter.await_ready()) { using handle_t = std::experimental::coroutine_handle<P>;
using await_suspend_result_t = decltype(awaiter.await_suspend(handle_t::from_promise(p)));
<suspend-coroutine>
if constexpr (std::is_void_v<await_suspend_result_t>){ awaiter.await_suspend(handle_t::from_promise(p)); <return-to-caller-or-resumer> } else { static_assert( std::is_same_v<await_suspend_result_t, bool>, "await_suspend() must return 'void' or 'bool'.");
if (awaiter.await_suspend(handle_t::from_promise(p))) { <return-to-caller-or-resumer> } }
<resume-point> }
return awaiter.await_resume();以封装一个socket的connect操作为例,我们希望能像这样在协程中去connect一个tcp地址:
coro_ret<int> connect_addr_example(io_service& service, const char* ip, int16_t port){ coroutine_tcp_client client; // 异步连接, service是对epoll的一个封装 auto connect_ret = co_await client.connect(ip, port, 3, service); printf("client.connect return:%d\n", connect_ret); if (connect_ret) { printf("connect failed, coroutine return\n"); co_return -1; }
do_something_with_connect(client);
co_return 0;}那么需要做的事情是
大致的代码如下:
struct connect_awaiter { coroutine_tcp_client& tcp_client_;
// co_await开始会调用,根据返回值决定是否挂起协程 bool await_ready(){ auto status = tcp_client_.status(); switch(status) { case ERROR: printf("await_ready: status error invalid, should not suspend!\n"); return true; case CONNECTED: printf("await_ready: already connected, should not suspend!\n"); return true; default: printf("await_ready: status:%d, return false.\n", status); return false; }
}
// 在协程挂起后会调用这个,如果返回true,会返回调用者,如果返回false,会立刻resume协程 bool await_suspend(std::coroutine_handle<> awaiting){ printf("await_suspend invoked.\n"); tcp_client_.handle_ = awaiting; return true; }
// 在协程resume的时候会调用这个,这个的返回值会作为await的返回值 int await_resume(){ int ret = tcp_client_.status() == CONNECTED ? 0 : -1; printf("awati_resume invoked, ret:%d\n", ret); return ret; } };了解了co_await之后,可以回头看一下之前的内容,前面多次出现的std::suspend_never和std::suspend_always就是两个预定义好的awaiter,也有那三个接口的定义,有兴趣的同学可以看看对应的源代码。promise对象的initial_suspend、final_suspend、yield_value返回的都是awaiter,实际上系统执行的是 co_await promise.initial_suspend() ,co_yield实际上执行的是 co_await promise.yield_value() 。如果有需要,也可以返回自定义的awaiter。

可以看出C++20给出了一个非常灵活、有很强大可定制性的协程机制,但缺少基本的库支持,连写一个最简单的协程都需要开发者付出不少理解和学习的成本,目前的状态只能说是打了一个的地基,在C++23中,为协程提供库的支持是重要的目标之一,可以拭目以待。
参考资料:
作者简介

杨良聪
腾讯后台开发工程师
腾讯后台开发工程师,毕业于华中科技大学,目前负责欢乐斗地主后端开发工作,有丰富的后台开发经验。
推荐阅读
GooseFS 在云端数据湖存储上的降本增效实践
新周期重构地产与物业数智化价值,TVP行业大使有话说
轻松上手!手把手带你掌握从Context到go设计理念
深入浅出带你走进Redis!

👇点击「阅读原文」,注册成为社区创作者,认识大咖,打造你的技术影响力!