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
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:
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
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 (
- Setup a continuation for each sub-operation you want to do