c++ 协程
协程的原理和应用
在一些高并发应用场景中如果使用多线程(每个请求一个线程),每个请求的大部分时间可能都是阻塞在IO上,会带来很多线程切换的开销(C/C++ 线程的切换是由内核控制的);多线程以外,一般有2种其它方案,一个是用IO多路复用(如 Nginx、Redis都用 epoll 实现非阻塞网络IO),另一个就是协程,就我了解目前各种语言里面对协程的原生支持最好的就是 golang,在runtime 实现了一个协程调度器,可以在一个物理线程里面并发执行上百个协程;
协程的定义
协程的本质是一个支持挂起(suspend)和恢复(resume)的函数,也就是说我们可以暂停执行这个函数(程序执行流会回到 Caller),去做其他事情,然后在恰当的时候再恢复到离开的位置继续执行。也就是说,普通的函数只能返回一次,而协程可以返回多次。
例如一个普通的函数:
void Fun() { std::cout << 1 << std::endl; std::cout << 2 << std::endl; std::cout << 3 << std::endl; std::cout << 4 << std::endl; }
Fun 是一个非常普通的函数,有4行代码,会依次执行;这个函数一旦开始,就无法暂停;
下面是一段 C++ 协程的不完整的例子
Result Coroutine() { std::cout << 1 << std::endl; co_await std::suspend_always{}; std::cout << 2 << std::endl; std::cout << 3 << std::endl; co_await std::suspend_always{}; std::cout << 4 << std::endl; };
Result 的定义后面再谈论,现在只需要知道 Result 是按照协程的规则定义的类型,这个函数体当中的 co_await std::suspend_always{};,其中 co_await 是个关键字,它的出现,通常来说就会使得当前函数(协程)的执行被挂起,程序执行流会回到调用者。
C++ 的协程语义
C++ 20 开始支持协程,各个编译器的支持情况可以参考 https://en.cppreference.com/w/cpp/compiler_support;
个人感觉 C++ 提供的协程语义比较原始,用的时候还需要做很多封装才行;
Awaiter
协程的函数体中的三个关键字:
- co_await,挂起协程,这个关键字是一个运算符,表达式操作的对象称为 可等待体(awaiter),awaiter 要实现3个函数;
-
// 返回true 表示已就绪,无需挂起,否则表示需要挂起; bool await_ready(); // await_ready() 返回 false 时被调用,参数 coroutine_handle 表示当前协程 // 返回值类型有多个可能, ? await_suspend(std::coroutine_handle<> coroutine_handle); // 协程恢复时被调用,其返回值就作为 co_await 表达式的返回值 T await_resume();
- 标准库当中提供了两个非常简单直接的等待体,struct suspend_always 表示总是挂起,struct suspend_never 表示总是不挂起;
-
- co_yield,可用于传值给协程的调用者;
- co_return,返回一个值或者从协程体返回;
所以,如果看见一个函数里面有 上面这样的关键字,那它就是一个协程;
编译器遇到 co_await 表达式,会将其翻译成下面的样子:
{ auto&& value = <expr>; auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value)); auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable)); 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(); }
可以看出来 await_suspend 的返回值:
-
如果是 void 或者 true,当前协程挂起之后将执行权还给当初调用者;
-
返回 false,则恢复执行当前协程;
标准库中提供了2个默认的 Awaiter 实现
namespace std { struct suspend_never { constexpr bool await_ready() const noexcept { return true; } // continue current coroutine constexpr void await_suspend(coroutine_handle<>) const noexcept {} constexpr void await_resume() const noexcept {} }; struct suspend_always { constexpr bool await_ready() const noexcept { return false; } // go back caller constexpr void await_suspend(coroutine_handle<>) const noexcept {} constexpr void await_resume() const noexcept {} }; }
Promise
协程的状态(返回值)可以用一个 Promise 对象来描述
struct Result { struct promise_type { ... }; };
promise_type 是一个接口,定义了协程执行节点中的一些回调;promise 对象会在 协程栈(coroutine frame)里面构建。
如果一个协程函数长这样
Result coroutine() { <body-statements> }
它实际执行的时候会被翻译成:
{ co_await promise.initial_suspend(); try { <body-statements> } catch (...) { promise.unhandled_exception(); } FinalSuspend: co_await promise.final_suspend(); }
一个 Result 的简单实现:
struct Result { struct promise_type { // 构建返回值对象 Result get_return_object() { return coroutine_handle<promise_type>::from_promise(*this); } suspend_never initial_suspend() noexcept { return {}; } suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() {} }; Result(coroutine_handle<promise_type> h) : handle(h) {} coroutine_handle<promise_type> handle; };
这个例子里面 get_return_object 返回的是 coroutine_handle,它指向了协程栈的地址,可以用来恢复协程的执行,或者销毁协程栈
namespace std::experimental { template<typename Promise> struct coroutine_handle; template<> struct coroutine_handle<void> { bool done() const; void resume(); void destroy(); void* address() const; static coroutine_handle from_address(void* address); }; template<typename Promise> struct coroutine_handle : coroutine_handle<void> { Promise& promise() const; static coroutine_handle from_promise(Promise& promise); static coroutine_handle from_address(void* address); }; }
应用
跨线程执行
如下图,左边灰色的coroutine 函数,当它被调用的时候,被切分成了两个部分,线程1在执行完成第一部分后,线程1就继续做其他事情了。
第二部分被suspend了。当时机出现的时候,线程2(可以是线程1)就开始执行第二部分。
#include <iostream> #include <experimental/coroutine> #include <string> #include <chrono> #include <thread> #include <future> #include <time.h> // g++ -std=c++2a -stdlib=libc++ -fcoroutines-ts test1.cc using namespace std; using std::chrono::duration_cast; using std::chrono::nanoseconds; using std::experimental::coroutine_handle; using std::experimental::noop_coroutine; using std::experimental::suspend_always; using std::experimental::suspend_never; #define LOG(MSG) printf("[%d][%ld] %s\n", std::this_thread::get_id(), time(nullptr), MSG); struct Awaiter { int value; std::future<void> future; bool await_ready() { // 协程挂起 LOG("await_ready") return false; } void await_suspend(coroutine_handle<> handle) { LOG("await_suspend begin") // 切换线程 future = std::async(std::launch::async, [=](){ using namespace std::chrono_literals; std::this_thread::sleep_for(3s); value = 100; // 恢复协程 auto* p_handle = const_cast<coroutine_handle<>*>(&handle); p_handle->resume(); }); LOG("await_suspend done") } int await_resume() { LOG("await_resume") // value 将作为 co_await 表达式的值 return value; } }; struct Result { struct promise_type { // 构建返回值对象 Result get_return_object() { return coroutine_handle<promise_type>::from_promise(*this); } // co_return 传入参数 void return_value(int value) { cout << "return_value = " << value << endl; } suspend_never initial_suspend() noexcept { return {}; } suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() {} }; Result(coroutine_handle<promise_type> h) : handle(h) {} coroutine_handle<promise_type> handle; }; Result coroutine() { int local = 777; cout << co_await Awaiter{.value = 0} << endl; LOG("after_suspend") co_return local; } int main() { Result co = coroutine(); LOG("destroy") co.handle.destroy(); cout << "main done" << endl; }
执行结果:
[1304852224][1692150804] await_ready [1304852224][1692150804] await_suspend begin [1304852224][1692150804] await_suspend done [1304852224][1692150804] destroy [193277952][1692150807] await_resume 100 [193277952][1692150807] after_suspend return_value = 777 main done
可以看到,主线程挂起后立即返回了,子线程在 resume 后会自动执行协程的后半部分代码,不需要主线程去阻塞等待子线程。
生成器
#include <iostream> #include <experimental/coroutine> #include <string> // g++ -std=c++2a -stdlib=libc++ -fcoroutines-ts main.cc using namespace std; using std::chrono::duration_cast; using std::chrono::nanoseconds; using std::experimental::coroutine_handle; using std::experimental::noop_coroutine; using std::experimental::suspend_always; using std::experimental::suspend_never; struct generator { typedef struct promise_type { generator get_return_object() noexcept { return coroutine_handle<promise_type>::from_promise(*this); } auto initial_suspend() noexcept { return suspend_never{}; } // 在 final_suspend() 挂起了协程,所以要手动 destroy auto final_suspend() noexcept { return suspend_always{}; } suspend_always yield_value(int value) { value_ = value; cout << value_ << endl; return {}; } void unhandled_exception() {} int value_; }; generator(coroutine_handle<promise_type> h) : handle(h) {} coroutine_handle<promise_type> handle; }; generator makeGenerator() { cout << "coroutine start " << endl; //co_await suspend_always{}; for (int i = 0; i < 10; i++) { co_yield i; // go back to caller } cout << "coroutine done" << endl; } int main() { generator coro = makeGenerator(); for (int i = 0; i < 10; i++) { coro.handle.resume(); // continue execute coroutine } cout << "destroy" << endl; coro.handle.destroy(); }
co_yield 表达式等价于 co_await promise.yield_value(<expr>)
执行结果:
coroutine start 0 1 2 3 4 5 6 7 8 9 coroutine done destroy
协程函数的调用栈
普通函数的调用栈都是在 stack 上嵌套,而协程的调用栈除了使用 stack,还会用到 heap 堆内心。
example:
void f() { // 普通函数 Result co = x(42); h(co); } Result x(int a) { // 协程 g(); co_await ... ... } int g() { // 普通函数 ... } void h(Result co) { co.handle.resume(); }
普通函数 f() 调用了 协程 x(int a),调用之前的栈
调用 x() 之后,跟普通函数调用一样,将函数返回值地址、函数入参、函数指针压入栈
协程执行的时候会在协程帧(coroutine frame)上构建 promise 对象,这里是在堆上分配内存,此时编译器也会在寄存器(rbp)里面存协程帧的指针
协程 x 里面又调用了一个普通函数 g(),x()的栈里面多了一个 coroframe 指针
当 g() 返回后,x() 的帧会被还原,同时 g() 的返回值 b 会被存入协程栈里面
接下来,协程x() 遇到 co_wait 表达式,假设被挂起后返回调用者 f(),协程返回时,一般返回给主调函数一个handle(执行协程帧),同时协程帧也会存一个RP地址(resumption-point),用于resume后找到协程挂起时的执行点。
返回到主调 f() 函数后,又继续执行 h(),并且在 h() 里面执行了 协程的 resume
参考资料:
https://lewissbaker.github.io/2017/09/25/coroutine-theory
https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await
https://lewissbaker.github.io/2018/09/05/understanding-the-promise-type
https://github.com/facebook/folly/tree/main/folly/experimental/coro