上文 [C++] C++20协程例子之惰性计算(附时序图) 已在目前MSVC的具体实现上, 大概描述了一个协程的整个工作流程.
本文继续在MSVC的具体实现上, 简述各个阶段中的一些细节.
与上文同理, 本文依旧用守护器称呼promise_type, 用同步器称呼awaiter. 另外有时称co_yield作用的对象为挂起值或发送值, 外部给co_yield提供的对象称为恢复值或接收值. 返回值只用于称呼co_return和return作用的对象. 具体如下:
协程函数
Lazy f(int a) {
for(int i = 0; i < a; ++i) {
co_yield i;
}
co_return 10;
}
近似实现如下
Lazy f(int a) {
// 协程分配内存
std::coroutine_traits<Lazy, int>::promise_type promise;
// 按照C++标准 promise 应先构造再调用 initial_suspend 但 MSVC 的实际实现是颠倒的
auto initial_awaiter = promise.initial_suspend();
// initial_awaiter 在主线程栈上普通构造, 然后在协程栈上移动构造, 然后在主线程栈上析构, 最后实为协程栈上的对象
promise::promise_type();
auto __return__ = promise.get_return_object(); // __return__ 在主线程栈上普通构造
// 该 __return__ 对象将在本协程函数挂起后给主线程函数作为协程函数的返回值
co_await initial_awaiter; // 该 initial_awaiter 对象生命周期一直持续到协程结束
try {
for(int i = 0; i < a; ++i) {
co_await promise.yield_value(i);
// co_yield exp 转化为等价表达式 co_await promise.yield_value(exp)
}
promise.return_value(10);
goto final_suspend;
// co_return exp; 转化为等价表达式 promise.return_value(exp); goto final_suspend;
// co_return; 转化为等价表达式 promise.return_void(); goto final_suspend;
} catch(...) {
promise.unhandled_exception();
goto final_suspend;
}
final_suspend:
co_await promise.final_suspend();
// initial_awaiter 析构
// 实参 析构
// promise 析构
// 协程回收内存
}
co_await是一个新的单目运算符. 运算符可重载.
当其作用在对象上时, 例如下
(co_await awaiter)
表达式近似实现如下
((awaiter.await_ready() ? : awaiter.await_suspend(this_coroutine_handle)), awaiter.await_resume())
整个表达式的类型由await_resume的返回类型决定.
当awaiter.await_ready返回false之后, 协程将自身挂起, 并将自身的句柄作为参数调用awaiter.await_suspend, 以方便未来恢复协程.
不管有没有调用awaiter.await_suspend挂起, 调用完awaiter.await_ready()返回true或者协程恢复后, 协程首先调用awaiter.await_resume, 取其返回值作为co_await表达式的结果.
虚线为可选路径.
initial_suspend返回的同步器其生命周期会和守护器一起, 伴随整个协程的生命周期. 其余同步器只是中途的匆匆过客.
个人猜测initial_suspend返回的同步器生命周期过长应该是BUG, 未来可能会修复.
另外个人还猜测initial_suspend的调用以后可能会延后到守护器调用完构造函数之后.
co_yield rvalue
表达式等价展开为 co_await promise.yield_value(rvalue)
.
假设在这次求值中, 协程挂起过, 那么全过程运行如下:
rvalue
右值构造.promise.yield_value(rvalue)
, 返回一个同步器对象awaiter
.awaiter.await_ready()
, 返回false
.awaiter.await_suspend(handle)
.awaiter.await_resume()
作为co_await表达式的结果.rvalue
右值析构. 协程挂起时, 外部可以像访问左值引用一样随意访问该rvalue
右值.
co_return rvalue
表达式等价展开为 promise.return_value(rvalue); goto final_suspend;
.
在直到协程调用final_suspend
试图做最后一次挂起前, 执行过程的顺序及细节如下:
rvalue
右值构造.promise.return_value(rvalue)
, 丢弃返回值.rvalue
右值析构.promise.final_suspend()
, 返回一个同步器对象awaiter
.awaiter.await_ready()
, 返回一个bool
值.bool
值为false
, 调用awaiter.await_suspend(handle)
. 在协程做最后一次挂起前, rvalue
右值析构了!. 也就是说, 想让return_value
去只记录rvalue
右值的地址, 然后在
final_suspend
时挂起, 让外部访问该右值, 这是不可以的.
要让外部访问rvalue
右值, 唯一的办法是在return_value
中移动或复制rvalue
.
下面提示一个C++用于存储任意值的占位对象. aligned_storage
, 通过模板参数可以设定该对象具有不小于给的大小的任意大小, 并做内存对齐. 若将其置于其他对象内, 则其他对象初始化时可不对该占位对象做任何处理.
用法如下
typename std::aligned_storage<sizeof(return_type)>::type return_value_buffer;
代码定义了一个名为return_type_buffer
的对象, 其大小sizeof(return_type_buffer)
大于等于sizeof(return_type)
, (多出来的空间是为了内存对齐).
我们可以在一个类内定义该任意值对象, 使得类初始化时, 不对该空间做任何处理(避免无谓的初始化和赋值移动).
稍后return_value
接收并移动rvalue
时, 可使用如下表达式(placement new, 置地new, 在给定空间上构建对象).
new(&return_value_buffer) return_type(std::move(rvalue));
使用以下表达式访问对象
*reinterpret_cast<const return_type*>(&return_value_buffer)
使用以下表达式析构对象
reinterpret_cast<return_type*>(&return_value_buffer)->~return_type();
另外亦可用联合体等实现不初始化对象.
为能让协程能直接访问恢复值, 而无需移动或复制, 外部向协程发送值后, 应立即恢复协程, 避免值析构.
co_yield exp
表达式的有等价展开式 co_await promise.yield_value(exp)
.
如果需要使用co_yield返回多种类型的对象, 可通过在守护器里提供多个yield_value函数重载来实现.
下面是重载的例子
HandleDeliver yield_value(int v) noexcept {
this->v = v;
return HandleDeliver(handle);
}
HandleDeliver yield_value(float v) noexcept {
this->v = (int)v;
return HandleDeliver(handle);
}
协程函数中co_yield现可接收float型对象:
Lazy f() {
for(int i = 0; i < 5; ++i) {
co_yield i;
}
co_yield 3.14f;
co_return 10;
}
守护器类中, return_value不可重载, return_void和return_value亦不可共存(包括以模板形式).
同时, 一个协程函数不允许有return表达式.
出现异常时, 协程中自动生成的catch语句会接收异常对象, 并无参调用unhandled_exception.
unhandled_exception函数内部需用std::current_exception()
获取该异常, 尔后可转移该异常或用std::rethrow_exception()
重抛和重接异常.
下面是处理异常的一个例子.
try {
std::rethrow_exception(std::current_exception());
} catch(std::exception& e) {
std::cout << "exception in stack: " << e.what() << std::endl;
} catch(...) {
std::cout << "unknow exception" << std::endl;
// 若不使用 std::current_exception 获取该异常, 则有内存泄漏风险, 特别是抛出的是在堆上创建的异常(throw new)
}
注意, 协程栈上建立的异常对象不能抛到协程外, 栈上异常将在unhandled_exception调用结束后析构, 若需将异常抛到外部, 该异常需为在堆上创建(throw new). 由于unhandled_exception无法检测异常对象的大小信息, 因此栈上异常不宜移动, 对于栈上异常, unhandled_exception必须自行处理.
throw new std::exception(); // 可将其抛到协程外
throw std::exception(); // 不可抛到协程外, 该栈上异常将在 unhandled_exception 结束后析构
调用final_suspend函数时, 函数若返回suspend_never()
对象,(即协程库中await_ready恒返回true的对象), 则线程不被挂起, 协程连同守护器等正常销毁并回收内存, 最后转移运行权给主线程.
注意此时守护器已被析构, 最终获取到运行权的主线程无法读取协程中co_return等的返回值及其他遗留的数据.
final_suspend可返回suspend_always()
(即协程库中await_ready恒返回false的对象)使线程临终时做最后一次挂起, 让主线程有机会读取遗留的数据(包括返回值, 异常等).
final_suspend返回suspend_always()
做最后一次挂起之后,主线程需要且只能调用coroutine_handle<>.destroy将协程销毁,否则将导致内存泄漏.而且由于协程的运行已经结束,而且不能调用coroutine_handle<>.resume以试图恢复协程!
据标准, 协程调用suspend_always并挂起后, 调用coroutine_handle<>.resume的行为为UB行为.
可使用CRT提供的机制进行检查.
main函数第一行加上
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
之后可在任意位置调用_CrtDumpMemoryLeaks()
在调试窗口输出泄漏信息(能输出包括地址,大小,申请内存的new所在源码的位置,前16字节内容等), 若无内存泄漏则无输出.
下面是一个内存泄漏的例子
int main() {
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
new int[100];
_CrtDumpMemoryLeaks();
return 0;
}
协程句柄对象的类型位于std::experimental命名空间. 以后可能会移动到std命名空间.
协程通过守护器获取到同步器后, 调用await_suspend前完成自我挂起, 并将自身句柄传送给await_suspend. await_suspend函数可随意恢复其他协程(包括自己的协程), 而不必担心协程的恢复发生嵌套.
标准里并未提到(或者我看漏了)同一个协程每次调用同步器的await_suspend函数进行挂起时, 给予的协程句柄是否都为同一个. 但测试中实际都为同一个, 即使是调用final_suspend返回的同步器的await_suspend函数进行挂起时, 给予的协程句柄依然为同一个. 由此可以断定同一个线程每次挂起时通过同步器给予的协程句柄都是同一个.
提取协程句柄的裸指针可调用address. 从裸指针重建协程句柄对象可使用coroutine_handle<>::from_address
静态函数.
另外, 可使用coroutine_handle
替换coroutine_handle<>
获得一个可访问守护器的协程句柄类型. 亦可使用守护器重建协程句柄. 下面是一个在外部通过协程的守护器来恢复协程的例子.
std::experimental::coroutine_handle<Lazy::promise_type>::from_promise(promise).resume();
从主线程调用resume函数, 或直接使用()
运算符即可. 例如下
handle.resume() // 1
handle() // 2
通过MSVC提供的头文件中的源码可以发现, 协程句柄的地址与守护器的地址差值是固定的, 也就是说, 一个守护器"陪伴"一个协程.
co_yield
表达式(或co_await
), 否则该协程与一普通函数功能相同, 没有协程的意义.co_yield
时(或co_await
), 必然产生一对过程: 对外发送对象(有时为void), 和从外接收对象(有时为void).void
).co_yield
表达式求值中, 协程向外发送了对象, 并在等待接收对象中.co_return
结束了, 协程向外发送了返回值, 并等待销毁.throw
抛出了个异常, 协程向外发送了异常对象, 并等待销毁.co_yield
挂起后, 外部处理该协程时, 要做的就是co_yield
发送的值(若非void). 给上文中所有函数都加上输出后, 得到如下结果.
其中函数名前的数字为this地址, await_suspend函数名后的数字为协程句柄的address返回的地址.
00735158 Lazy::promise_type::initial_suspend
006FF454 HandleDeliver::HandleDeliver
007351B8 HandleDeliver::HandleDeliver
006FF454 HandleDeliver::~HandleDeliver
00735158 Lazy::promise_type::promise_type
00735158 Lazy::promise_type::get_return_object
006FF7B0 Lazy::Lazy
007351B8 HandleDeliver::await_ready
007351B8 HandleDeliver::await_resume
f()
f(): 0
00735158 Lazy::promise_type::yield_value
00735178 HandleDeliver::HandleDeliver
00735178 HandleDeliver::await_ready
00735178 HandleDeliver::await_suspend 00735168
&result: 006FF7B0
>0
00735178 HandleDeliver::await_resume
00735178 HandleDeliver::~HandleDeliver
f(): 1
00735158 Lazy::promise_type::yield_value
00735178 HandleDeliver::HandleDeliver
00735178 HandleDeliver::await_ready
00735178 HandleDeliver::await_suspend 00735168
>1
00735178 HandleDeliver::await_resume
00735178 HandleDeliver::~HandleDeliver
f(): 2
00735158 Lazy::promise_type::yield_value
00735178 HandleDeliver::HandleDeliver
00735178 HandleDeliver::await_ready
00735178 HandleDeliver::await_suspend 00735168
>2
00735178 HandleDeliver::await_resume
00735178 HandleDeliver::~HandleDeliver
f(): 3
00735158 Lazy::promise_type::yield_value
00735178 HandleDeliver::HandleDeliver
00735178 HandleDeliver::await_ready
00735178 HandleDeliver::await_suspend 00735168
>3
00735178 HandleDeliver::await_resume
00735178 HandleDeliver::~HandleDeliver
f(): 4
00735158 Lazy::promise_type::yield_value
00735178 HandleDeliver::HandleDeliver
00735178 HandleDeliver::await_ready
00735178 HandleDeliver::await_suspend 00735168
>4
00735178 HandleDeliver::await_resume
00735178 HandleDeliver::~HandleDeliver
f() return
00735158 Lazy::promise_type::return_value
00735158 Lazy::promise_type::final_suspend
0073518C HandleDeliver::HandleDeliver
0073518C HandleDeliver::await_ready
0073518C HandleDeliver::await_suspend 00735168
0073518C HandleDeliver::~HandleDeliver
007351B8 HandleDeliver::~HandleDeliver
00735158 Lazy::promise_type::~promise_type
result.get_return(): 10
完整测试源码如下, 欢迎自行验证.
#include
#include
class HandleDeliver {
HandleDeliver() = delete;
HandleDeliver(const HandleDeliver&) = delete;
HandleDeliver& operator= (const HandleDeliver&) = delete;
HandleDeliver& operator= (HandleDeliver &&) = delete;
private:
std::experimental::coroutine_handle<>& phandle;
bool is_ready;
public:
HandleDeliver(std::experimental::coroutine_handle<>& phandle, bool is_ready=false) noexcept :
phandle(phandle),
is_ready(is_ready) {
std::cout << this << " " << __FUNCTION__ << std::endl;
}
HandleDeliver(HandleDeliver &&self) noexcept :
phandle(self.phandle) {
std::cout << this << " " << __FUNCTION__ << std::endl;
}
~HandleDeliver() {
std::cout << this << " " << __FUNCTION__ << std::endl;
}
bool await_ready() const noexcept {
std::cout << this << " " << __FUNCTION__ << std::endl;
return is_ready;
}
void await_suspend(std::experimental::coroutine_handle<> handle) noexcept {
std::cout << this << " " << __FUNCTION__ << " " << handle.address() << std::endl;
phandle = handle;
}
void await_resume() const noexcept {
std::cout << this << " " << __FUNCTION__ << std::endl;
}
};
class Lazy {
Lazy() = delete;
Lazy(const Lazy&) = delete;
Lazy& operator= (const Lazy&) = delete;
Lazy& operator= (Lazy &&) = delete;
public:
class promise_type {
promise_type(const promise_type&) = delete;
promise_type(promise_type &&) = delete;
promise_type& operator= (const promise_type&) = delete;
promise_type& operator= (promise_type &&) = delete;
private:
std::experimental::coroutine_handle<> handle;
int v;
bool is_overed;
public:
HandleDeliver initial_suspend() noexcept {
std::cout << this << " " << __FUNCTION__ << std::endl;
return HandleDeliver(handle, false);
}
promise_type() noexcept : v(-1), is_overed(false) {
std::cout << this << " " << __FUNCTION__ << std::endl;
}
Lazy get_return_object() noexcept {
std::cout << this << " " << __FUNCTION__ << std::endl;
return Lazy(*this);
}
HandleDeliver yield_value(int v) noexcept {
std::cout << this << " " << __FUNCTION__ << std::endl;
this->v = v;
return HandleDeliver(handle);
}
HandleDeliver yield_value(float v) noexcept {
std::cout << this << " " << __FUNCTION__ << std::endl;
this->v = (int)v * 10;
return HandleDeliver(handle);
}
void return_value(int v) noexcept {
std::cout << this << " " << __FUNCTION__ << std::endl;
this->v = v;
}
HandleDeliver final_suspend() noexcept {
std::cout << this << " " << __FUNCTION__ << std::endl;
is_overed = true;
return HandleDeliver(handle);
}
void unhandled_exception() {
try {
std::rethrow_exception(std::current_exception());
} catch(std::exception& e) {
std::cout << "exception: " << e.what() << std::endl;
} catch(...) {
std::cout << "unknow exception" << std::endl;
}
}
~promise_type() noexcept {
std::cout << this << " " << __FUNCTION__ << std::endl;
}
public:
void resume() const noexcept {
handle.resume();
}
bool overed() const noexcept {
return is_overed;
}
int value() const noexcept {
return v;
}
void destroy() noexcept {
handle.destroy();
}
};
private:
class iterator_type {
iterator_type() = delete;
iterator_type(const iterator_type&) = delete;
iterator_type& operator= (const iterator_type&) = delete;
iterator_type& operator= (iterator_type &&) = delete;
private:
Lazy &z;
public:
explicit iterator_type(Lazy& z) noexcept : z(z) {}
iterator_type(iterator_type &&self) noexcept : z(self.z) {}
iterator_type& operator++ () noexcept {
z._next();
return *this;
}
bool operator!= (iterator_type const& other) const noexcept {
return !z.is_overed;
}
int operator* () const noexcept {
return z._get();
}
};
private:
promise_type & promise;
bool is_overed;
int retv;
protected:
bool _next() {
if(is_overed) {
return false;
}
promise.resume();
// promise.rethrow_if_has_exception();
if(promise.overed()) {
retv = promise.value();
promise.destroy();
is_overed = true;
return false;
}
return true;
}
int _get() const noexcept {
if(is_overed) {
return -1;
} else {
return promise.value();
}
}
public:
explicit Lazy(promise_type& promise) noexcept :
promise(promise),
is_overed(false),
retv() {
std::cout << this << " " << __FUNCTION__ << std::endl;
}
Lazy(Lazy &&self) noexcept :
promise(self.promise),
is_overed(self.is_overed) {
std::cout << this << " " << __FUNCTION__ << std::endl;
}
iterator_type begin() {
return iterator_type(*this);
}
iterator_type end() {
return iterator_type(*this);
}
int get_return() {
if(is_overed) {
return retv;
} else {
return -1;
}
}
};
Lazy f() {
std::cout << "f()" << std::endl;
for(int i = 0; i < 5; ++i) {
std::cout << "f(): " << i << std::endl;
co_yield i;
}
std::cout << "f() return" << std::endl;
co_return 10;
}
int main() {
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
{
Lazy result = f();
std::cout << "&result: " << &result << std::endl;
for(int i : result) {
std::cout << ">" << i << std::endl;
}
std::cout << "result.get_return(): " << result.get_return() << std::endl;
}
_CrtDumpMemoryLeaks();
getchar();
return 0;
}