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

91 阅读1分钟

原文地址: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。

这对于解决一个竞赛条件来说,是一件很麻烦的事情。而且事实证明,我在列出我们可以保存同步对象的各种地方时,漏掉了一个地方。我们下次再继续讨论。