c++20 协程本质
背景:
最近因项目关系,web端,js异步调用,发现跟本门的C++20 还是有些不一样的,本文主要从另外一个角度来看
什么是协程
协程是能暂停执行以在之后恢复的函数。协程是无栈的:它们通过返回到调用方暂停执行,并且恢复执行所需的数据与栈分离存储。这样就可以编写异步执行的顺序代码(例如不使用显式的回调来处理非阻塞输入/输出),还支持作用于惰性计算的无限序列上的算法及其他用途。
腾讯用c语言实现了一个有栈携程,libco,有兴趣的同学可以看下,跟本文讲的协程不太一样。
当一个函数中出现 co_yeild, co_wait, co_return,它就是一个协程
了解一些概念
承诺(promise)对象,从协程内部操纵。协程通过此对象提交其结果或异常。
协程句柄 (coroutine handle),从协程外部操纵。这是用于恢复协程执行或销毁协程帧的非拥有柄
co_yeild, co_wait, co_return ,可以参照https://zh.cppreference.com/w/cpp/language/coroutines里边自行看下,
我们主要分析协程在编译后会变成什么样子
示例代码
C++ #include <coroutine> #include <exception> #include <iostream> #include <thread>
struct Generator {
class ExhaustedException : std::exception {};
struct promise_type { int value; bool is_ready = false;
std::suspend_always initial_suspend() { return {}; };
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(int value) { this->value = value; is_ready = true; return {}; }
void unhandled_exception() {
}
Generator get_return_object() { return Generator{std::coroutine_handle<promise_type>::from_promise(*this)}; }
void return_void() {} };
std::coroutine_handle<promise_type> handle;
bool has_next() { if (handle.done()) { return false; }
if (!handle.promise().is_ready) { handle.resume(); }
if (handle.done()) { return false; } else { return true; } }
int next() { if (has_next()) { handle.promise().is_ready = false; return handle.promise().value; }
}
explicit Generator(std::coroutine_handle<promise_type> handle) noexcept : handle(handle) {}
~Generator() { if (handle) handle.destroy(); } };
Generator fibonacci() { co_yield 0; co_yield 1;
int a = 0; int b = 1; while (true) { co_yield a + b; b = a + b; a = b - a; } }
int main() { auto generator = fibonacci(); for (int i = 0; i < 10; ++i) { if (generator.has_next()) { std::cout << generator.next() << " " << std::endl; } else { break; } } return 0; } |
原示例很简单,斐波那契数列,考虑以下问题:
上文中的fibonacci方法中,int a; int b 存在哪里,是栈空间还是堆空间
为什么fibonacci方法返回的Generator里边会定义struct promise_type 内部结构体
协程的帧到底长什么样子的
协程挂起与恢复的本质是什么
带着上边疑问,接下来就开始分析下,编译器把这块代码转成什么样子的
C++
struct __fibonacciFrame { void (*resume_fn)(__fibonacciFrame *); void (*destroy_fn)(__fibonacciFrame *); std::__coroutine_traits_impl<Generator>::promise_type __promise; int __suspend_index; bool __initial_await_suspend_called; int a; int b; std::suspend_always __suspend_71_11; std::suspend_always __suspend_72_3; std::suspend_always __suspend_73_3; std::suspend_always __suspend_78_5; std::suspend_always __suspend_71_11_1; };
Generator fibonacci() { /* Allocate the frame including the promise */ __fibonacciFrame * __f = reinterpret_cast<__fibonacciFrame *>(operator new(__builtin_coro_size())); __f->__suspend_index = 0; __f->__initial_await_suspend_called = false; /* Construct the promise. */ new (&__f->__promise)std::__coroutine_traits_impl<Generator>::promise_type{}; /* Forward declare the resume and destroy function. */ void __fibonacciResume(__fibonacciFrame * __f); void __fibonacciDestroy(__fibonacciFrame * __f); /* Assign the resume and destroy function pointers. */ __f->resume_fn = &__fibonacciResume; __f->destroy_fn = &__fibonacciDestroy; /* Call the made up function with the coroutine body for initial suspend. This function will be called subsequently by coroutine_handle<>::resume() which calls __builtin_coro_resume(__handle_) */ __fibonacciResume(__f); return __promise.get_return_object(); }
/* This function invoked by coroutine_handle<>::destroy() */ void __fibonacciDestroy(__fibonacciFrame * __f) { /* destroy all variables with dtors */ __f->~__fibonacciFrame(); /* Deallocating the coroutine frame */ operator delete(__builtin_coro_free(static_cast<void *>(__f))); } |
方便分析,上边代码已经删除了一部分,首先我们先回答第一个问题,这两个变量存放位置,
struct __fibonacciFrame 里边有int a; int b;两个成员,在fibonacci(),可以看到使用了new 关键字分配对象,也就是说int a;int b;被编译器放到堆里边了,
第二个问题:
为什么要定义struct promise_type,通过上边的代码也可以看出来, new (&__f->__promise) std::__coroutine_traits_impl<Generator>::promise_type{}; 萃取机已经明确要求,里边要含有promise_type, 并且这个里边要有明确的几个协程必备的方法定义。另外通过__promise.get_return_object()可以构建Generator 对象,
第三个问题:
协程帧的样子就是struct __fibonacciFrame这个里边定义的,包含协程恢复与销毁的方法
第四个问题:
协程挂起与恢复,本质上讲就是函数调用,可以看下下边代码,协程恢复本质上就是标记一个值,每次调用的时候根据这个值,goto 到指定的代码位置,协程里边局部成员都保存在堆里边了,可以直接使用,
C++ /* This function invoked by coroutine_handle<>::resume() */ void __fibonacciResume(__fibonacciFrame * __f) { try { /* Create a switch to get to the correct resume point */ switch(__f->__suspend_index) { case 0: break; case 1: goto __resume_fibonacci_1; case 2: goto __resume_fibonacci_2; case 3: goto __resume_fibonacci_3; case 4: goto __resume_fibonacci_4; } /* co_await insights.cpp:71 */ __f->__suspend_71_11 = __f->__promise.initial_suspend(); if(!__f->__suspend_71_11.await_ready()) { __f->__suspend_71_11.await_suspend(std::coroutine_handle<Generator::promise_type>::from_address(static_cast<void *>(__f)).operator coroutine_handle()); __f->__suspend_index = 1; __f->__initial_await_suspend_called = true; return; } __resume_fibonacci_1: __f->__suspend_71_11.await_resume(); /* co_yield insights.cpp:72 */ __f->__suspend_72_3 = __f->__promise.yield_value(0); if(!__f->__suspend_72_3.await_ready()) { __f->__suspend_72_3.await_suspend(std::coroutine_handle<Generator::promise_type>::from_address(static_cast<void *>(__f)).operator coroutine_handle()); __f->__suspend_index = 2; return; } __resume_fibonacci_2: __f->__suspend_72_3.await_resume(); /* co_yield insights.cpp:73 */ __f->__suspend_73_3 = __f->__promise.yield_value(1); if(!__f->__suspend_73_3.await_ready()) { __f->__suspend_73_3.await_suspend(std::coroutine_handle<Generator::promise_type>::from_address(static_cast<void *>(__f)).operator coroutine_handle()); __f->__suspend_index = 3; return; } __resume_fibonacci_3: __f->__suspend_73_3.await_resume(); __f->a = 0; __f->b = 1; while(true) { /* co_yield insights.cpp:78 */ __f->__suspend_78_5 = __f->__promise.yield_value(__f->a + __f->b); if(!__f->__suspend_78_5.await_ready()) { __f->__suspend_78_5.await_suspend(std::coroutine_handle<Generator::promise_type>::from_address(static_cast<void *>(__f)).operator coroutine_handle()); __f->__suspend_index = 4; return; } __resume_fibonacci_4: __f->__suspend_78_5.await_resume(); __f->b = (__f->a + __f->b); __f->a = (__f->b - __f->a); } goto __final_suspend; } catch(...) { if(!__f->__initial_await_suspend_called) { throw ; } __f->__promise.unhandled_exception(); } __final_suspend: /* co_await insights.cpp:71 */ __f->__suspend_71_11_1 = __f->__promise.final_suspend(); if(!__f->__suspend_71_11_1.await_ready()) { __f->__suspend_71_11_1.await_suspend(std::coroutine_handle<Generator::promise_type>::from_address(static_cast<void *>(__f)).operator coroutine_handle()); } ; } |
协程恢复就更好理解了,
C++ if(!__f->__suspend_73_3.await_ready()) { __f->__suspend_73_3.await_suspend(std::coroutine_handle<Generator::promise_type>::from_address(static_cast<void *>(__f)).operator coroutine_handle()); __f->__suspend_index = 3; return; } |
await_ready return false,就直接进入里边,然后就return了,很好理解
总结:
通过上边的理解,我们可以看到协程本质上还是函数调用,利用goto语句来实现协程挂起与恢复