TPL tricks
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:
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 (generallyTaskScheduler.Default
) or useTask.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