.NET

Task Parallel Library – TaskScheduler, Threads, and Deadlocks

Task Parallel Library makes your code perform faster, look nicer (aka review), and easier to maintain (aka fix bugs, and extend). However, there are a few things you need to be aware of while using it, otherwise you may end up with a strangely behaving application, or worse, you will find yourself forever waiting for a task to be executed <– not cool, not cool at all.

The mighty TaskScheduler

One of the most important notions you need to grasp when using tasks is TaskScheduler. The TaskScheduler represents a mechanism that is responsible for scheduling tasks for execution.

At the time of writing this post, there are 3 TaskSchedulers in .NET framework 4.5:

  • ThreadPoolTaskScheduler

    • uses the ThreadPool to schedule tasks for execution
    • the scheduler by itself is not very smart nor does it do a lot, however the ThreadPool is extremely smart and fast. A few noticeable features of it are: lock-free mechanism for storing/retrieving user work items, work stealing (threads from the tread pool will steal work from each other when they have nothing to do) – you can read more here
  • SynchronizationContextTaskScheduler

    • uses the current synchronization context (SynchronizationContext.Current) and posts the task to it – you can read more here
    • If called from the UI thread, Silverlight, Windows Presentation Foundation, and WinForms, SynchronizationContext.Current will return a synchronization context that will always execute work on the UI thread
    • used by calling TaskScheduler.FromCurrentSynchronizationContext()
    • VERY IMPORTANT – there are multiple implementations of the SyncronizationContext, and it is not always safe to assume that a synchronization context is bound to only one thread, therefore when using TaskScheduler.FromCurrentSynchronizationContext() you need to be on the UI thread, this is the only way to guarantee that you will retrieve back a task scheduler that schedules tasks on the UI thread
  • ConcurrentExclusiveTaskScheduler

    • Is a hybrid that operates in two modes
      • ProcessingExclusiveTask – processes the scheduled tasks in an exclusive manner (one at a time, no two tasks run at the same time)
      • ProcessingConcurrentTasks – processes the scheduled tasks in a concurrent manner, and allows to specify a max concurrency level (how many tasks can run at the same time)
    • Used internally by ConcurrentExclusiveSchedulerPair
    • You can read more here

 

While creating tasks and continuations you are very likely to stumble upon two static properties of the TaskScheduler

  1. TaskScheduler.Default
    • Returns an instance of the ThreadPoolTaskScheduler
  2. TaskScheduler.Current
    • If called from within an executing task will return the TaskScheduler of the currently executing task
    • If called from any other place will return TaskScheduler.Default

 

Threads

Depending on how you create and run your tasks, different schedulers will be used, thus a variating threading behavior will be exhibited.

private void InstantiateAndStart()
{
  var task = new Task(() => { });
  task.Start(); 
  //will use TaskScheduler.Current to schedule the task for execution
}

private void UsingTheTaskFactory()
{
  Task.Factory.StartNew(() => { }); 
  //will use TaskScheduler.Current to schedule the task for execution
}

private void UsingTaskRun()
{
  Task.Run(() => { });
  //will use TaskScheduler.Default to schedule the task for execution
}

private void ExecutingOnANewThread()
{
  //both tasks defined bellow will be executed on a new background thread
  var task = new Task(() => { }, TaskCreationOptions.LongRunning);
  task.Start(TaskScheduler.Default);

  //or

  var task2 = Task.Factory.StartNew(() => { }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}

Important facts

  • starting a new task does not necessary spawn a brand new thread, in certain conditions it will execute on background thread, and in others will execute on the same thread that it was started. It all depends on the TaskScheduler that gets used to schedule the task.
  • if you want your task to execute on a brand new thread, use the ExecutingOnANewThread example above, and keep in mind that it is up for the task scheduler to decide what to do with your TaskCreationOptions.LongRunning. The ThreadPoolTaskScheduler will always create a new background thread when presented with this task creation option, however other task schedulers may exhibit a different behavior.
  • the constructs that use the ambiental TaskScheduler.Current can easily become tricky, therefore it is recommended to use the UsingTaskRun example when you want your task to execute asynchronously, and always specify your TaskScheduler when using the other constructs, as it makes your code more explicit, and less likely for people to get in trouble when working with it.

Deadlocks and unexpected behaviors

Trying to do something on a background thread and ending up executing on the UI thread

Take the following example

Task.Factory.StartNew(() => PerformSlowOperation())
.ContinueWith(loadTask =>
{
   //load the loadTask.Result in the UI
   //now you decide you want to do some more slow operations and start a new task

   Task.Factory.StartNew(() => PerformSlowOperation());
}, TaskScheduler.FromCurrentSynchronizationContext());

The example illustrates a simple scenario in which a task is started to perform a slow operation, then a continuation is hocked up to the task to print the results to the screen, hence the usage of TaskScheduler.FromCurrentSynchronizationContext(), afterwards a new slow operation is triggered.

Q: On which thread will the second invocation of the PerformSlowOperation execute, background or UI thread?

A: UI thread

Why:

  1. Task.Factory.StartNew uses TaskScheduler.Current, on the first invocation of PerformSlowOperation, the TaskScheduler.Current was not defined, therefore TaskScheduler.Default was used => execution on a background thread
  2. The continuation was executed on the UI thread, because it was requested by specifying the synchronization context specific TaskScheduler
  3. When PerformSlowOperation was called the second time, the TaskScheduler.Current was no longer undefined, but it was actually pointing to the TaskScheduler of the continuation, and the TaskScheduler of the continuation schedules work to be executed on the UI thread.

Fix:

  • Use a task construct that can take a task scheduler when executing PerformSlowOperation the second time
  • Use TaskCreationOption.HideScheduler when executing PerformSlowOperation the second time
  • etc

Deadlocking

Consider the following case

new Task(() =>
{
   //executing on the UI thread

   //starting a new task and waiting for it
   var otherTask = Task.Factory.StartNew(() => PerformSlowOperation());
   otherTask.Wait();
}).Start(TaskScheduler.FromCurrentSynchronizationContext());

The above example illustrates a scenario when a task is executing on the UI thread, then decides to spawn a new task to do some lightweight work and wait for it to finish.

Q: What will happen when the above code gets run?

A: The application will hang forever

Why: Thanks to TaskScheduler.Current the invocation of the PerformSlowOperation method will be scheduled on the UI thread, but it will never get the chance to start, because the current task, which also executes on the UI thread, is blocking the thread waiting for it to finish executing.

Fix:

  • Do not wait on the task, setup a continuation
  • Use a task construct that can take a task scheduler for otherTask
  • Use TaskCreationOption.HideScheduler on the otherTask
  • etc

The above is by no means an extensive list of what can go wrong when using tasks, however the benefits outweigh the potential pitfalls, and one should not be afraid of using tasks. However, if you find yourself in a tricky situation remember that your are not alone, Visual Studio includes a task visualizer that can help you understand what’s happening.

Visual Studio Task Visualizer

 

The above print screen of the Task Visualizer depicts the deadlocking example. You can observe the first task executing on the main thread, and the second task being just scheduled for execution – too bad it will never get executed. But the Task Visualizer doesn’t necessary indicate that we are in a deadlock situation, it is still up to the developer to switch to each task, understand how they relate to each other, and identify the deadlock.

Task Parallel Library introduction

Task Parallel Library is a multi-threading framework that you can use in your .net daily life. The framework aims at easying the development of multi-threaded applications by offering a nice API on top of Thread and ThreadPool.

The main character in TPL is Task, and it represents a unit of work that should be executed asynchronously.

Creating and running a task

Can be achieved though one of the following contraptions:

  • simple instantiate and run
Console.WriteLine("Main runs on thread {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);

var myTask = new Task(() =>
   {
      Console.WriteLine("My task runs on thread {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
   });

myTask.Start();
  • using the TaskFactory offered by Task, or Task<TResult>
Console.WriteLine("Main runs on thread {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);

var myTask = Task.Factory.StartNew(() =>
   {
      Console.WriteLine("My task runs on thread {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
   });
  • using Task.Run
Console.WriteLine("Main runs on thread {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);

var myTask = Task.Run(() =>
   {
      Console.WriteLine("My task runs on thread {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
   });

Calling the steps required to create and run a Task a contraption is a bit harsh, but in my opinion there are too many ways to start and run a task. Using any of the above constructions will eventually/hopefully achieve you the same result, as they only differ in the amount of customization that you can specify.

Further understanding tasks

Means getting used with the following

TaskCreationOptions

  • None – you have no idea what you’re doing and just want to let TPL do its job
  • PreferFairness – The default task scheduler that ships with .net uses a global queue where it enqueues tasks to be executed, then as threads from the thread pool become available, tasks will get dequeued and assigned to the now available thread. This is what happens for top level tasks (tasks not created within another task), for nested tasks, a separate behavior is exhibited, those tasks don’t end up on the global queue, instead they get queued in a local queue belonging to the current running thread. When it becames free, the current thread will first try to pickup work from his own local queue, and only if there’s nothing there will try to pickup work from the global queue. An important thing to note here, is that picking up work from the local queue hapens in a LIFO (Last In First Out) fashion, while picking up work from the global queue takes place in a FIFO (First In First Out) manner. Now that we worked our way to this point, we can finally explain what PreferFairness does. It indicates that a newly started task should always be enqueued on the global queue, regardless if it is a child task or not (thus causing tasks to be executed in the order they were started). If you want to read the full story about task queuing, and how threads can steal work from each other, make sure to follow this link
  • AttachedToParent – by default, if you have nested tasks, they will be treated individually, meaning that a parent task can complete before its children complete, or the other way around, there is no synchronization between child and parrent tasks. If you specify AttachedToParent all child tasks will become “attached” to their parent tasks, and the parent task will only complete when the child tasks complete (simply said, the parent task will wait for its children to finish)
  • DenyChildAttach – this option allows a parent task to prevent any child tasks from attaching to it. An important note here is that by default a child task will not attempt to attach itself to the parent, it is up to the programmer to request this behavior, through the use of AttachedToParent
  • LongRunning – gives a hint to the TPL library that the task is expected to take more than just a few seconds to complete. The default task scheduler will then execute the task on a brand new thread, and not a thread from the ThreadPool.
  • HideScheduler – will cause the task to be scheduled on TaskScheduler.Default, and not on a potential TaskScheduler.Current <– more about this will follow in a new post

Important note – you can specify more than one task creation option by bitwise OR-ing multiple options (e.g.: TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach)

CancellationToken

Each task has a CancellationToken that can be used to prematurely terminate a task.

Important note – terminating a task doens’t just happen out of the blue, it is the programmers responsability to periodically evaluate from within the task if cancellation has been requested, and gracefully terminate if requested (can do it easily by calling token.ThrowIfCancellationRequested()).

Important note – If a task cancellation is requested before TPL gets the chance to actually start the task, the now cancelled task will not be started at all, and will complete immediately.

TaskScheduler

Will evaluate the task requirements and schedule it for execution accordingly (on a new thread, threadpool thread, etc)

Task chaining

Task chaining allows a task to start executing only when a specific task has finished (faulted, etc). This feature fits nicely with common UI data loading/processing situations, when you want to perform some lengthily operation on a background thread and only transition to the UI thread when you want to display the results of the operation.

Debug.WriteLine("Application starts up on thread {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);

Task<ProcessingResult>.Run(() =>
{
   Debug.WriteLine("Doing some lengthily operation on thread {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
   return new ProcessingResult();
})
.ContinueWith((prevTask) =>
{
   ProcessingResult prevTaskResult = prevTask.Result;
   Debug.WriteLine("Showing the operation results to the user on thread {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);

}, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.FromCurrentSynchronizationContext());

The output of the above code is

Application starts up on thread 8
Doing some lengthly operation on thread 9
Showing the operation results to the user on thread 8

Note – in the above example you can also observe how you can return information from one task and how it gets passed to the continuation

TaskContinuationOptions

Chaining tasks requires understanding the TaskContinuationOptions enum:

  • None – as described above
  • PreferFairness – as described above
  • AttachedToParent – as described above
  • DenyChildAttach – as described above
  • LongRunning – as described above
  • HideScheduler – as described above
  • ExecuteSynchronously – requests the continuation to run synchronously, meaning that after the first task finishes, the continuation will be executed on the same thread
  • LazyCancellation – no point in me describing it, just follow this link and scroll down to LazyCancellation
  • NotOnCanceled – does not execute the continuation if the first task was canceled
  • NotOnFaulted – does not execute the continuation if the first task had an unhandled exception
  • NotOnRanToCompletion – does not execute the continuation if the first task ran without exceptions
  • OnlyOnCanceled – NotOnFaulted | NotOnRanToCompletion
  • OnlyOnFaulted – NotOnCanceled | NotOnRanToCompletion
  • OnlyOnRanToCompletion – NotOnCanceled | NotOnFaulted

Important note – you can specify more than one task continuation option by bitwise OR-ing multiple options (e.g.: TaskContinuationOptions.LongRunning | TaskContinuationOptions.OnlyOnRanToCompletion )

Using tasks is a nice way to introduce multi threading in your application, and, if used with a bit of care, will make your code faster and easier to read. However, there are a few edge cases that can cause unexpected results. However, I will cover these cases in a future post.