原文地址:devblogs.microsoft.com/oldnewthing…
原文作者:devblogs.microsoft.com/oldnewthing…
发布时间:2019年12月26日
上一次,我们又做了一次尝试,修复了C++/WinRT的resume_foreground(DispatcherQueue)函数在试图恢复在dispatcher队列上执行时的竞赛条件。我们的做法是让队列任务等到 await_suspend 完成后才允许 coroutine 恢复执行,我们找到了一个很好的地方来放置同步对象,即在 awaiter 中,但即使有了这个修复,我们还是在热代码路径中引入了额外的内存障碍。
但事实证明,这些工作都是不必要的。我们只需要换一种方式来看待这个问题。
将TryEnqueue的结果存储到m_queued中的目的是为了让 await_resume可以报告lambda是否被排队。但是我们可以用另一种方式来推断这些信息。事实上,我们的lambda正在运行,这就意味着得到了队列。因为如果lambda没有被排队,那么它一开始就不会运行。
这使得我们可以简化waiter,让lambda负责报告它被排队了。
bool await_suspend(coroutine_handle<> handle)
{
// m_queued =
return
m_dispatcher.TryEnqueue([this, handle]
{
m_queued = true;
handle();
});
// return m_queued;
}
有两种情况需要考虑。
首先,TryEnqueue可能失败。在这种情况下, await_suspend返回false,m_queued继续保持原来的值(也是false)。coroutine立即在同一线程上恢复, await_ready将返回m_queued,也就是false。m_queued的值正确报告lambda没有被排队。
否则,TryEnqueue成功了,这是比较有意思的情况。由于 await_suspend 在调用 TryEnqueue 后不访问任何成员变量,所以不管 lambda 是在 await_suspend 返回之前还是之后运行,都没有关系。
await_suspend返回true是因为lambda被排队了,这就允许暂停coroutine的运行。没有人更新m_queued,所以它的初始值仍然是false。这是一个不正确的状态,但没关系:我们会在任何人注意到之前修正它。
当 lambda 运行时,它将 m_queued 设置为 true。这就使m_queued成员变量的值与实际发生的情况一致,从而恢复了宇宙的平衡。只有在修复了m_queued之后,我们才会调用句柄。这两个操作(更新m_queued和调用句柄),所以我们在设置m_queued和在 await_ready中观察到的m_queued之间不存在竞赛条件。
你可以说我们懒惰地更新了m_queued成员变量。在 await_suspend 中更新它是不安全的,所以我们要等到 lambda。我们不必把true的值显式地传递给lambda,因为lambda知道,如果lambda正在运行,true是唯一可能的值。
我们对C++ coroutine的介绍到此结束。我甚至还没有机会去了解创建coroutine所需要的承诺和其他基础架构¹。到目前为止,我们只是在研究创建可等待对象所需要的基础架构。总有一天,我会写关于承诺的文章,但我要休息一下。
额外的唠叨。注意到我最初解决这个问题的本能 是写了50多行代码。但停下来想想让我把它缩减到一半左右。然后退一步看更大的问题,让我通过对两行代码的小改动来解决这个问题。
¹这意味着,在我们了解寻找等待者的神秘步骤1之前,我们必须等待。