April 7th, 2020

Creating a non-agile delegate in C++/WinRT, part 2: The synchronous coroutine

Last time, we saw that you could use an ICallback­Context to run code synchronously in another apartment from your delegate, which is important if the code that is calling your delegate is relying on the timing of your return.

We can also express this in the form of a coroutine that operates synchronously.

If we make the await_suspend invoke the handle synchronously, then the continuation of the coroutine runs synchronously with the code that called co_await.

auto resume_synchronous(ICallbackContext* context)
{
  struct awaiter : std::experimental::suspend_always
  {
    ICallbackContext* context;
    bool await_suspend(
        std::experimental::coroutine_handle<> handle)
    {
      InvokeInContext(context, handle);
      return true;
    }
  };
  return awaiter{ context };
}

This simplifies the delegate by letting you use co_await to do the dirty work.

deviceWatcher.Added(
    [=, context = CaptureCurrentApartmentContext()]
    (auto&& sender, auto&& info) -> winrt::fire_and_forget
    {
        co_await resume_synchronous(context.Get());
        viewModel.Append(winrt::make<DeviceItem>(info));
    });

Even though there is a co_await, execution continues synchronously because await_suspend runs the continuous synchronously.

Whether co_await resumes synchronously or not¹ is determined by the awaiter. If you co_await something whose awaiter resumes asynchronously, then the co_await will resume asynchronously.

deviceWatcher.Added(
    [=, context = CaptureCurrentApartmentContext()]
    (auto&& sender, auto&& info) -> winrt::fire_and_forget
    {
        auto original_context = CaptureCurrentApartmentContext();
        co_await resume_synchronous(context.Get());
        viewModel.Append(make<DeviceItem>(info));
        co_await resume_synchronous(original_context.Get());
        more_stuff();
        auto result = co_await GetMoreDataAsync();
        process_result(result);
    });

In the above example, the first two co_awaits are synchronous, but the third one (co_await GetMoreDataAsync()) is presumably asynchronous. This means that the delegate will return at the point of the third co_await, and reference parameters (sender and info) are probably not going to be valid when the coroutine resumes.

¹ Or at all. The built-in awaiter suspend_always suspends and never wakes up.

 

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

2 comments

Discussion is closed. Login to edit/delete existing comments.

  • 紅樓鍮

    In the last example, the device watcher thread is waiting synchronously for the UI thread, but then the UI thread sends work back to the device watcher thread and waits synchronously for that to complete. Isn't that a deadlock, or is the marshaling infrastructure magically able to execute callbacks in the device watcher's context while the thread is waiting in an InvokeInContext call? Or is InvokeInContext not an inter-thread synchronous wait even in...

    Read more
    • Raymond ChenMicrosoft employee Author

      The UI thread sent work back to the original context, not necessarily the same thread. Since the original context was the multi-threaded apartment, the work ended up being sent to some other thread in the thread pool.