The Task Parallel Library is a tricky piece of software and occasionally it exhibits a behavior that is not expected.

I got reminded of one of those behavior today which may be hard to apprehend at first:

or, be careful not to nest task in task when the outer task is on a sync context scheduler and you are waiting on the inner task

So now for the longer version.

As you are probably aware, the lowest building block of the TPL are Task that get executed on TaskScheduler. The default scheduler is based on the work-stealing threadpool introduced in .NET 4.0 which essentially means your tasks are executed by default on random threads.

Although there is a default scheduler, the library lets anyone write its own specific scheduler so that you can execute Task the way you want. Another scheduler is actually exposed in the framework through the TaskScheduler.FromCurrentSynchronizationContext method.

The scheduler this method creates essentially wraps the SynchronizationContext object of the thread its created on to proxy task execution to this thread.

SynchronizationContext are traditionally used with loop-based GUI library to give developers a way to execute code on the main loop thread from a different one. It’s a standard pattern for a lot of frameworks like Winforms, MonoTouch or Mono for Android.

Now imagine the following situation:

// From the main thread, we launch an async operation
var asyncOp = Task.Factory.StartNew (() => GetSomeData ());
// We setup a continuation to be run on the main thread thanks to the sync context scheduler
asyncOp.ContinueWith (t => {
		// We can touch mainloop owned objects
		myLabel.Text = t.Result;

		// We send an ack to the endpoint that gave us the data
		var task = Task.Factory.StartNew (() => AckDataReceived (t.Result));

		// Wait for ack to finish
		task.Wait ();
}, TaskScheduler.FromCurrentSynchronizationContext ());

This code will deadlock, can you spot why?

Answer: one of the TPL behavior is that the default scheduler used for Task creation is NOT the default threadpool scheduler but rather the last scheduler that was run in the parent context.

So here, when we where expecting the AckDataReceived task to execute on a different thread, it’s actually using the previously set SynchronizationContext-based TaskScheduler to execute the Task which queues it on the main loop.

Since we are executing on the main loop, the next call to Wait will block the thread, making it impossible to execute the Task we are waiting on resulting in a deadlock.

There a couple of possible workarounds for this case:

  • If you nest Task, always specify the scheduler you want to use (generally TaskScheduler.Default) or use Task.Run (which use the default scheduler right away)
  • Use the standard invoke of the toolkit to execute code on the main thread (Component.Invoke, RunOnUiThread, InvokeOnMainThread, …)
  • Setup a continuation for each sub-operation you want to do