[Windows翻译]C++的coroutines。DispatcherQueue任务运行过快的问题,第一部分。

256 阅读2分钟

原文地址:devblogs.microsoft.com/oldnewthing…

原文作者:devblogs.microsoft.com/oldnewthing…

发布时间:2019年12月23日

我在C++/WinRT的resume_foreground函数中,当它试图恢复在dispatcher队列上的执行时,偶尔会遇到崩溃的情况。下面是该函数的简化版本。

auto resume_foreground(DispatcherQueue const& dispatcher)
{
  struct awaitable
  {
    DispatcherQueue m_dispatcher;
    bool m_queued = false;

    bool await_ready()
    {
      return false;
    }

    bool await_suspend(coroutine_handle<> handle)
    {
      m_queued = m_dispatcher.TryEnqueue([handle]
        {
          handle();
        });
      return m_queued;
    }

    bool await_resume()
    {
      return m_queued;
    }
  };
  return awaitable{ dispatcher };
}

关于DispatcherQueue对象,你需要知道的是,TryEnqueue方法会接受一个委托,并安排它在dispatcher队列的线程上运行。如果它无法做到这一点(比如说,因为线程已经退出了),那么该函数就会返回false。TryEnqueue方法的返回值是co_await的结果。

让我们来演练一下这个函数的工作原理。

resume_foreground方法返回一个对象,作为自己的等待者。当co_await发生时,coroutine首先调用 await_ready,返回false,意思是 "继续暂停我"。

接下来,coroutine会调用 await_suspend。这个方法试图将coroutine的恢复排队到dispatcher线程上,并在m_queued成员变量中记住它是否成功。

返回m_queued的值意味着如果继续成功排队(true),则coroutine保持暂停,直到调用句柄时恢复。另一方面,如果延续没有被成功调度(false),则放弃暂停,并立即在同一线程上恢复执行。

无论哪种方式,当coroutine恢复时,都会被告知是否重新调度到调度器线程上成功。

好了,现在你明白了它的工作原理,你能发现它的缺陷吗?

这段代码违反了我们在开始使用 awaitable 对象时给出的一条规则。一旦你安排了句柄的调用 你就不能访问任何成员变量 因为在async_suspend完成之前 coroutine可能已经恢复了

这就是这里发生的事情。派遣器队列在运行lambda 甚至在async_suspend将答案保存到m_queued中之前 结果,代码崩溃了(如果你很幸运)或者破坏了内存(如果你不幸运)。

所以我们需要确保lambda不会在async_suspend之前进行竞赛。

下一次,我们将首次尝试修复这个问题。

(事实上,我称它为第一次尝试,这给了你一个线索,它可能需要不止一次的尝试。)


wwww.deepl.com 翻译