That’s an issue happening from time to time. You need to do some lengthy computation on the UI thread (for instance, a page navigation), and you want to display a progress indicator to ease up the user waiting. It’s possible on WinRT thanks to the compositor thread: a dedicated thread for animations that don’t impact the page layout, making them run smoothly even when the UI thread is busy. Some built-in controls automatically use the compositor thread, such as the ProgressRing. So, what the issue could be?
Letting the UI refresh
Let’s say that when the user taps on a button, we want to display a progress indicator, start a computation that can only run on the UI thread, then hide the progress indicator. Given those requirements, the code I first wrote was:
This code doesn’t work. The UI freezes for a few seconds (as many as needed by the DoSomething method), but no progress indicator is displayed. Why? Because the UI is refreshed only when the event handler has finished executing. But when our event handler returns, we have already finished the computation and hid the progress indicator. For this code to work, we must give a chance to the UI thread to run between the lines 3 and 5. How can we do this? We could rewrite the code to use Dispatcher.RunAsync:
It works, but it feels a bit clumsy. Is there a way to avoid using lambdas? That’s when I thought about using the await keyword:
What happens under the hood is that the compiler rewrites the method to start a timer after showing the progress indicator, then returns, giving a chance for the thread to refresh the UI. After one millisecond, the timer ticks, and the remainder of the method is executed back in the UI thread (by a mechanism we’ll detail later in this article. Teaser). This code works as expected, so I moved to another subject, but I was still bothered by the need to start a timer. Sure, it’s more readable than using Dispatcher.RunAsync, still it feels like a hack. Is there a cleaner way to achieve the same result? A few weeks later, I ran across the Task.Yield method. And, lo and behold, it does exactly what I needed! It yields control to the calling thread and dispatches the remainder of the method, without having to use a timer. I got back to my app and eagerly fixed my code:
I compiled, ran the application, tapped on the button, and sure enough it worked. On a hunch, I tapped a second time on the button and… it stopped working.
The seemingly inconsistent behavior is reproduced consistently: on each run of the application, the code works the first time the button is tapped. On every subsequent taps, the UI is freezed and the progress indicator isn’t displayed, just as if Task.Yield wasn’t called. What’s going on?
To get a clearer understanding, I started peeling off the abstraction layers. The await keyword is really just a convenient way to dispatch a call to the current SynchronizationContext. Therefore, I rewrote my code to call it manually:
Sure enough, the same behavior is reproduced. Can we dig further? I won’t get into the technical details, but for applications with an UI, the SynchronizationContext is an abstraction around the Dispatcher. However, we already wrote the same code using the Dispatcher earlier in this article, and it worked. It means that the issue lies in the way the SynchronizationContext calls the Dispatcher. The Dispatcher has but two parameters, the only one that could change is the priority. What is the priority used by the SynchronziationContext? Fortunately, the Dispatcher has a CurrentPriority property, so it’s just a matter of displaying it:
A look at the debug panel in Visual Studio shows that the call is made with “normal” priority. In my previous sample, I used “low” priority. Rewriting it with “normal” priority brings the same bogus behavior. We’ve found the cause. Now let’s try to understand what’s happening.
Under the hood
Because it works only the first time, I quickly came to suspect a timing issue. Methods traditionally take longer to execute the first time, for various reasons: it hasn’t been optimized yet by the JIT compiler, some static constructors may run, some additional dependencies may be loaded, and so on. The UI thread is an infinite loop, trying to refresh the UI at a constant 60 frames per second while processing events and executing the callback enqueued by the dispatcher. In our case, the thread has two methods to execute: the part of the button event where we show the progress indicator, and the subsequent call where we do the lengthy calculation and hide the progress indicator. What if, when the first method executes really quickly, the dispatcher decided that it has enough time to execute another callback before refreshing the UI?
Keeping a steady 60 FPS means that the UI has to be refreshed every 16 milliseconds. When we use Task.Delay, the timer initialization followed by the 1 ms of waiting time we set give enough time for the dispatcher to decide to refresh the UI. Task.Yield is much faster, and the callback is executed before the dispatcher had a chance to refresh the UI. Also, refreshing the UI is considered by the dispatcher as “normal” priority. When we enqueue a callback with low priority, those callbacks are put in hold until the UI is refreshed, bypassing the issue altogether.
So, is there no other way than awaiting Task.Delay or using Dispatcher.RunAsync? Interestingly, there is a Dispatcher.Yield method in WPF, built for this precise use-case. It does pretty much the same thing as Task.Yield, except that it enqueues the callback with a low priority. For some reason, this method isn’t available in WinRT. Let’s hope that Microsoft fixes that oversight in the near future, but until then, could we make our own?
The async/await mechanism is very flexible. If you want more information on how to extend it, I suggest to read this article. In this case, we’ll extend the Dispatcher by using method extensions, and add a Yield method. The code is pretty much the same as used in the WPF version, shamelessly ripped off after decompiling the assemblies.
From there, we can rewrite our code to await Dispatcher.Yield, and have the expected behavior, without having to rely on a timer: