原文地址:devblogs.microsoft.com/oldnewthing…
原文作者:devblogs.microsoft.com/oldnewthing…
发布时间:2019年12月25日
上一次,我们修复了C++/WinRT的resume_foreground(DispatcherQueue)函数在试图恢复在dispatcher队列上的执行时的一个竞赛条件。我们的做法是让队列中的任务等到 await_suspend 完成后再让 coroutine 恢复执行。困难的部分是找到一个放置同步对象的地方,最后我们把它放在队列任务的lambda中。
但事实证明,我们还可以把它放在另一个地方,而且它一直在我们面前。
我们可以把它放在 awaiter 中。
auto resume_foreground(DispatcherQueue const& dispatcher)
{
struct awaitable
{
DispatcherQueue m_dispatcher;
bool m_queued = false;
slim_event ready;
bool await_ready()
{
return false;
}
bool await_suspend(coroutine_handle<> handle)
{
bool result = m_dispatcher.TryEnqueue([this, handle]
{
ready.wait();
handle();
});
m_queued = result;
ready.signal();
return result;
}
bool await_resume()
{
return m_queued;
}
};
return awaitable{ dispatcher };
}
当coroutine恢复时,waiter会被销毁,而coroutine的恢复要么是在调用coroutine句柄时(我们在lambda中做了),要么是在 await_ready或 await_suspend方法表明应该立即进行恢复时。
因此,只要我们没有做这些事情,waiting的成员都是好的。
在TryEnqueue成功的情况下, await_suspend是不会要求立即恢复的。当lambda调用处理程序时,恢复将发生。但是我们的lambda在调用处理程序之前,会等待async_suspend事件的信号,所以此时的等待者仍然有效。
在TryEnqueue失败的情况下,lambda永远不会运行。相反,在 await_suspend 返回一个值为 false 的值后,coroutine 会恢复。在返回之前,等待者仍然有效,所以我们可以对事件发出信号。(注意,没有人在监听信号。)
这比试图从lambda中挖掘事件要简单得多。
然而,这个计划还是有相当多的开销。虽然在非阻塞的情况下,slim_event确实是完全在用户模式下运行的, 但它仍然会产生大量的内存障碍。slim_event::signal方法使用了内存屏障,以确保在将signaled设置为true之前,m_queued对所有线程可见。如果不这样做,那么wait函数就会看到signaled为真,然后用错误的m_queued值来运行。
此外,在signal方法更新signalaled成员后,它会调用WakeByAddressAll,它本身会建立另一个内存屏障,以确保它找到了所有的waiter(即使没有)。
内存屏障是必要的,以确保在调度线程上继续执行coroutine的情况下,由 await_suspend写入的m_queued的更新值可以被 await_ready准备好。
下一次,我们将摆脱内存障碍。