This small post covers an old issue that’s still relevant today: accidentally blocking the main interface thread. In fact, it’s likely to be even more noticeable these days given the excellent job done creating the Parallel library in .NET 4 and the elegant simplicity it provides in its use.
Scenario
So how do you accidentally go about blocking the main UI thread? Well, very easily actually: we typically start on the main UI thread, and spawn a task to perform some background operation. Now when that operation completes, it’s likely that you need to update some UI based elements so you may use a ContinueWith on the aforementioned task with the main interface TaskScheduler specified?
Simple Task Example
public AsyncCollection(IEnumerable<T> listToLoad, TaskScheduler uiScheduler)
{
_isBusy = true;
_task = Task.Factory.StartNew(() => listToLoad.ToList())
.ContinueWith(load => FinishedLoading(load.Result),
CancellationToken.None,
TaskContinuationOptions.None, uiScheduler);
}
So in the simple example above, we’re loading a collection asynchronously and when this is done, we need to put those results back but on the main UI context.
However, assume you have access to an AsyncCollection (our simple example class above) of a particular type, and whereas normally you are more than happy for the update to occur asynchronously, you need to get an instant result before continuing? What would you do? Well, a quick look at the MSDN documentation would most likely reveal the Wait method on task, and 10 seconds later you have added the following method to the AsyncCollection class:
Blocking Wait
public IEnumerable<T> Wait()
{
if (_task != null)
{
_task.Wait();
}
return this;
}
Now, this is where the blocking occurs: if in your main thread you call Wait() on your AsyncCollection instance, you end up deadlocked! The initial component of our task can execute just fine, but the continuation cannot as it needs to occur on the main UI thread and that is currently blocked as we are waiting on that!
A Solution
Age/experience does have benefits (sometimes!) as anyone that has previously written threaded windows applications would think “Pump Messages” or “Do Events”. Basically we do not want to block the main UI thread; we want to continue the WPF message loop. You would normally associate this with keeping the UI visually alive whilst awaiting a result, but the same issue/solution is also applicable to our current less visible scenario.
A quick google later revealed such a method cleverly/logically called WaitWithPumping that is explained in some detail at ParallelExtensionsExtras Tour - #15 - Specialized Task Waiting
Non-blocking Wait
public static void WaitWithPumping(Task task)
{
if (task == null) throw new ArgumentNullException("task");
var nestedFrame = new DispatcherFrame();
task.ContinueWith(_ => nestedFrame.Continue = false);
Dispatcher.PushFrame(nestedFrame);
task.Wait();
}
(At Planet we have the above both as a static method and extension method depending on your preference/need.)
BTW The Parallel Programming team have an excellent blog called Parallel Programming with .NET that is well worth a visit!
Applying the Solution
To apply the solution to our scenario, only a few trivial steps are required…the main consideration we have is not making references that restrict our reuse!
Our AsyncCollection class is in a common/shared assembly that does not have any windows assemblies referenced, so we cannot directly use the aforementioned code; we need to take a step back and do something like:
C#
public IEnumerable<T> WaitForLoad(Action<Task> uiFriendlyWait)
{
if (_task != null)
{
uiFriendlyWait(_task);
}
return this;
}
The actual WaitWithPumping can be added to your shared windows assembly.
Then up in your user interface aware layers you can provide a joining of the two in a friendly accessible fashion (i.e. extension method!):
C#
public static class AsyncCollectionExtensions
{
public static IEnumerable<T> WaitWithPumping<T>(this AsyncCollection<T> collection)
{
if (collection == null) { throw new ArgumentNullException("collection"); }
return collection.WaitForLoad(task => TaskMethods.WaitWithPumping(task));
}
}
So now in those cases we need to await the result of an asynchronous operation before continuing and avoid the blocking resultant from multiple UI context access, we can simply tag on our extension method:
C#
var result = Model.Descriptors.WaitWithPumping();
Print | posted on Sunday, 5 December 2010 10:08 AM