原文
避免分配内存
异步操作一般需要存储一些仅在操作中有的跟踪操作进度的每操作状态.
如,调用异步Win32I/O函数,要分配并传递OVERLAPPED结构指针.调用者确保完成操作前它有效.
传统基于回调的API,要在堆上分配此状态,以确保它有适当生命期.多个,则要为每个操作分配和释放它.
如果有性能问题,则可用自定义分配器从池中分配.
但是,使用协程时,可利用挂起协程时,协程帧中的局部变量是活的,从而避免为操作状态分配堆存储.
在等待器对象中保存每操作状态,可有效从协程帧中"借用"内存,以便在协待式时存储它.
完成操作后,恢复协程并析构等待器对象,从而释放协程帧中内存.
最终,协程帧仍可能按堆分配.但是,一旦分配,协程帧就可来仅用单个堆分配来完成异步操作.
把协程帧当作真正高性能的分配内存器.编译时计算出所有局部变量期望总内存大小,然后可无成本按需分配此内存给局部变量!
示例:实现简单的线程同步原语
异步手动重置事件.
基本要求是,需要多个并发协程是可等待的,且等待时要挂起等待的协程,直到已恢复等待协程,且某个线程调用.set()方法.如果某个线程已调用了.set(),则应该继续而不挂起协程.
最好,还想使它为无异,非堆分配且无锁.示例如下:
T 值;
异步手动重置事件 事件;
//单个调用来生成值
空 生产者()
{值 = 一些长运行计算();//通过设置事件来发布值.事件.置();
}
//支持多个并发用户
任务<> 消费者()
{//在`生产者()`函数中,等待`事件.置()`发出信号.协待 事件;//现在消费"值"是安全的,保证在赋值给"值"后.标::输出 << 值 << 标::行尾;
}
先考虑该事件可能的状态:"未置"和"已置".
"未置"状态时,有个等待置它的等待协程列表(可能是空的).
为"已置"状态时,不会有等待协程,因为协待此状态事件的协程可继续运行而不会挂起.
该状态可用单个std::atomic<void*>来表示.
1,为"已置"状态保留特殊指针值.本例中,使用事件的this指针,因为知道该指针不可能与列表项地址相同.
2,否则,事件为"未置"状态,且该值是等待协程结构的单链表指针.
在协程帧中的"等待器"对象中存储节点,可避免额外调用堆上的链表分配节点.
因此,从此类接口开始:
类 异步手动重置事件
{
公:异步手动重置事件(极 初始置 = 假) 无异;//无需复制/移动异步手动重置事件(常 异步手动重置事件&) = 删;异步手动重置事件(异步手动重置事件&&) = 删;异步手动重置事件& 符号=(常 异步手动重置事件&) = 删;异步手动重置事件& 符号=(异步手动重置事件&&) = 删;极 是已置() 常 无异;构 等待器;等待器 符号 协待() 常 无异;空 置() 无异;空 重置() 无异;
私:友 构 等待器;//本为=>已置状态//否则为`=>`未置,`等待器*`链表的头部.可变 标::原子<空*> m状态;
};
在此,有个相当直接和简单接口.此时要注意它有个返回未定义等待器的协待()操作符方法.
现在定义等待器.
定义等待器
首先,要知道它在等待哪个异步手动重置事件对象,因此它需要事件引用及构造器来初化它.
还需要充当等待器值链接列表中的节点,因此要有列表中下个等待器对象的指针.
还要存储执行协待式等待协程的协柄,以便事件可在协程变为"已置"时恢复协程.
不必关心协程的承诺类型是什么,所以只使用协柄<>.
最后,要实现等待器接口,因此三个特殊方法:直接协,挂起协和恢复协.不想从协待式返回值,恢复协因此可返回空.
放在一起,等待器的基本接口如下:
构 异步手动重置事件::等待器
{等待器(常 异步手动重置事件& 事件) 无异: m事件(事件){}极 直接协() 常 无异;极 挂起协(标::实验性::协柄<> 等待协程) 无异;空 恢复协() 无异 {}
私:常 异步手动重置事件& m事件;标::实验性::协柄<> m等待协程;等待器* m下个;
};
现在,协待事件时,如果已置事件,不想等待挂起协程.因此,如果已置事件,可定义直接协()返回真.
极 异步手动重置事件::等待器::直接协() 常 无异
{中 m事件.是已置();
}
接着,看看挂起协()方法.这一般是大多数可等待类型的神奇的地方.
首先,它需要存储等待协程的协程句柄到m等待协程成员中,这样事件稍后可对它调用.恢复()方法.
然后,一旦完成,需要试原子方式把等待器排队到等待链列表中.如果成功加入,则返回true以指示不想立即恢复协程.否则,如果发现事件已并发更改为"已置"状态,则返回假以指示应立即恢复协程.
极 异步手动重置事件::等待器::挂起协(标::实验性::协柄<> 等待协程) 无异
{//指示是否`"置"`事件状态的特殊`m状态`值.常 空* 常 置状态 = &m事件;//记住等待协程的句柄.m等待协程 = 等待协程;//试原子方式把此`等待者`推到列表头.空* 旧值 = m事件.m状态.加载(标::获取内存序);干{//如果已在"置"状态,请立即恢复.如 (旧值 == 置状态) 中 假; //更新链表以指向当前头.m下个 = 静转<等待器*>(旧值);//最后,试交换旧的列表头,按新列表头插入该等待器.} 当 (!m事件.m状态.弱比交( 旧值, 本, 标::释放内存序, 标::获取内存序));//已成功入列.保持挂起.中 真;
}
注意,在加载旧状态时使用"获取"内存序,以便如果读取特殊的"set"值时,可查看调用"set()"前的写入.
如果成功比较交换,需要"释放"语义,以便后续调用"set()"时,可看到写入m等待协程和先前写入的协程状态.
填写事件类的其余部分
现在已定义了等待器,再看看异步手动重置事件方法.
首先是构造器.它要按"未置"状态(即nullptr)初化或按"已置"状态(即this)初化空的等待列表.
异步手动重置事件::异步手动重置事件( 极 初始置) 无异
: m状态(初始置?本:空针)
{}
接着,is_set()方法非常简单,如果有以下特殊值,则它是"已置":
极 异步手动重置事件::是已置() 常 无异
{中 m状态.加载(标::获取内存序) == 本;
}
接着是reset()方法.如果为"已置",则改为"未置",否则不变.
空 异步手动重置事件::重置() 无异
{空* 旧值 = 本;m状态.强比交(旧值, 空针, 标::获取内存序);
}
set()方法,通过用特殊的'已置'值this,来交换当前状态来过渡到'已置'状态,然后检查旧值.如果有等待协程,则在返回前依次恢复每个协程.
空 异步手动重置事件::置() 无异
{//需要"释放",以便后续的`"协待"`可见之前的写入.需要"获取",以便通过`等待协程`来查看先前的写入.空* 旧值 = m状态.交换(本, 标::内存序取释放);如 (旧值 != 本){//不是"已置"状态.按已取且需要恢复的等待链接列表头取旧值.动* 等待 = 静转<等待器*>(旧值);当 (等待 != 空针){//在恢复协程之前读`m下个`,因为恢复协程可能会析构等待器对象.动* 下个 = 等待->m下个;等待->m等待协程.恢复();等待 = 下个;}}
}
最后,要实现协待()操作符.这只需要构造一个等待器对象.
异步手动重置事件::等待器
异步手动重置事件::符号 协待() 常 无异
{中 等待器{ *本 };
}
好了.一个可等待的无锁,无分配内存,无异实现的异步手动重置事件.