Last time, we saw that you could use an ICallbackContext
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_await
s 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.
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...
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.