原文
异步操作一般需要存储一些仅在操作中有的跟踪操作进度
的每操作状态
.
如,调用异步Win32I/O
函数,要分配并传递OVERLAPPED
结构指针.调用者
确保完成操作前它有效.
传统基于回调的API
,要在堆
上分配此状态,以确保它有适当生命期.多个,则要为每个
操作分配和释放
它.
如果有性能问题,则可用自定义分配器
从池中分配
.
但是,使用协程
时,可利用挂起
协程时,协程帧中的局部变量
是活的,从而避免
为操作状态分配
堆存储.
在等待器
对象中保存每操作状态
,可有效从协程帧
中"借用
"内存,以便在协待
式时存储
它.
完成操作
后,恢复协程并析构等待器
对象,从而释放
协程帧中内存.
最终,协程帧
仍可能按堆分配.但是,一旦分配,协程帧就可来仅用单个
堆分配来完成异步
操作.
把协程帧
当作真正高性能的分配
内存器.编译时计算出所有局部变量期望总内存大小
,然后可无成本
按需分配此内存
给局部变量!
异步手动重置
事件.
基本要求是,需要多个并发协程是可等待
的,且等待时要挂起
等待的协程,直到已恢复等待协程
,且某个线程调用.set()
方法.如果某个线程已调用了.set()
,则应该继续而不挂起协程
.
最好,还想使它为无异
,非堆分配且无锁
.示例如下:
T 值;
异步手动重置事件 事件;
//单个调用来生成值
空 生产者()
{
值 = 一些长运行计算();
//通过设置事件来发布值.
事件.置();
}
//支持多个并发用户
任务<> 消费者()
{
//在`生产者()`函数中,等待`事件.置()`发出信号.
协待 事件;
//现在消费"值"是安全的,保证在赋值给"值"后.
标::输出 << 值 << 标::行尾;
}
先考虑该事件可能的状态:"未置"和"已置"
.
"未置
"状态时,有个等待置它的等待
协程列表(可能是空的).
为"已置"
状态时,不会有等待
协程,因为协待
此状态事件的协程
可继续运行而不会挂起
.
该状态可用单个std::atomic
来表示.
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等待协程.恢复();
等待 = 下个;
}
}
}
最后,要实现协待()
操作符.这只需要构造一个等待器对象
.
异步手动重置事件::等待器
异步手动重置事件::符号 协待() 常 无异
{
中 等待器{ *本 };
}
好了.一个可等待的无锁,无分配内存
,无异
实现的异步
手动重置事件.