原文地址:devblogs.microsoft.com/oldnewthing…
原文作者:devblogs.microsoft.com/oldnewthing…
发布时间:2019年12月24日
上一次,我们发现C++/WinRT的resume_foreground(DispatcherQueue)函数在试图恢复dispatcher队列上的执行时,存在一个竞赛条件。让我们试着修复它。
提醒一下,问题出在这里。
bool await_suspend(coroutine_handle<> handle)
{
m_queued = m_dispatcher.TryEnqueue([handle]
{
handle();
});
return m_queued;
}
问题的核心是lambda可能会在 await_suspend有机会将TryEnqueue()的结果保存到m_queued中之前就运行完成(包括destructing awaiter),导致存储到一个被释放的对象。
我们需要确保lambda等待 await_suspend完成工作后,lambda才继续恢复coroutine。
我们还需要注意,这是一个罕见的竞赛条件,所以我们希望在普通情况下保持快速。
这建议我,我们应该使用一个轻量级的同步基元,而不是像内核事件这样的重物。如果我们能在不采取内核过渡的情况下解决整个问题,那就太好了。
所以我们先从一个轻量级的同步对象开始。内存的单个字节,其地址可以被等待。(我们之前已经做过了)。
struct slim_event
{
slim_event() = default;
// Not copyable
slim_event(slim_event const&) = delete;
void operator=(slim_event const&) = delete;
bool signaled = false;
void signal()
{
std::atomic_thread_fence(std::memory_order_release);
WakeByAddressAll(&signaled);
}
void wait()
{
// Wait for "signaled" to be "not false" (i.e., true)
bool False = false;
while (!signaled) {
WaitOnAddress(&signaled, &False, sizeof(False), INFINITE);
}
std::atomic_thread_fence(std::memory_order_acquire);
}
};
我们需要在 await_suspend 和 lambda 之间共享这个同步对象。我们可以把它放在哪里呢?
在lambda中。 在 await_suspend 函数中。 两者都不放。在堆上作为一个shared_ptr. 我不打算为它做一个新的堆分配,因为这首先会让我们失去使用轻量级同步基元的部分好处。
那么在 await_suspend 函数中呢?
await_suspend函数的返回速度很快,而lambda可能会在队列中坐很久才最终运行。如果我们把 slim 事件放在 await_suspend 函数中,它必须等待 lambda 完成 slim 事件后才能安全地销毁它并返回。
这样就剩下lambda了。让lambda等待 await_suspend是可以的,因为它不需要等待很长时间,大多数时候它根本不需要等待。
我们希望能够做的是得到一个坐在lambda里面的变量的地址。
让我们承认这一点。没有简单的方法可以做到这一点。(虽然有一些困难的方法。)
我想出了这个主意。当lambda从调用站点被移到函数参数中 然后从函数参数移到委托中时 我们让lambda跟踪它被移到的地方 然后更新一个通过引用共享的变量 需要注意的是,这就要求lambda一旦被放在委托人里面就停止移动。
struct tracked_slim_event
{
tracked_slim_event(slim_event*& p)
: tracker(p) { tracker = &value; }
tracked_slim_event(tracked_slim_event&& other)
: tracker(other.tracker) { tracker = &value; }
slim_event*& tracker;
slim_event value;
};
tracked_slim_event包装了一个slim_event,但同时也管理着一个 "tracker",它是一个指针,当对象被创建时,它被设置为指向slim事件,并在对象被移动时更新。这可以让你找到对象的最终安息地。
更新构造函数参数是一种控制的反转。与其说有一个方法会在请求时告诉你答案, 不如说你传入了接收答案的东西, 对象会随着答案的变化而更新它.
现在我们可以让 await_suspend 函数访问隐藏在 lambda 中的 slim_event。
bool await_suspend(coroutine_handle<> handle)
{
slim_event* finder;
bool result = m_dispatcher.TryEnqueue(
[handle, tracker = slim_event_tracker(finder)] mutable
{
tracker.value.wait();
handle()
});
m_queued = result;
finder->value.signal();
return result;
}
趁我们还能保存TryEnqueue的结果到m_queued中。只有在它被安全地存储后,我们才通过发出slim事件的信号来释放lambda。
这对于解决一个竞赛条件来说,是一件很麻烦的事情。而且事实证明,我在列出我们可以保存同步对象的各种地方时,漏掉了一个地方。我们下次再继续讨论。