Deadlocks when using Tasks

TPLWhat is wrong with this code?

 public static class NiksMessedUpCode
    {
        private static async Task DelayAsync()
        {
            await Task.Delay(1000);
        }

        public static void Test()
        {
            // Start the delay.
            var delayTask = DelayAsync();
            // Wait for the delay to complete.
            delayTask.Wait();
        }
    }

 

The code compiles without errors yet there is something really terrible about this small piece of code that can effectively kill your application if you are not careful.

Can you spot what it is?

Basically when used in an ASP.NET or a GUI app (Windows Forms, WPF) this code will cause a deadlock. The application would stall and become completely unresponsive if this very small piece of innocent looking code is executed within the app. Surprisingly though the same code will work inside a console application! Go on give it a go!

So what mysterious dark forces are at play here?

The answer is the subtle differences in “context”. Basically when an incomplete Task is awaited the current “context” is captured and is used to run the remainder of the code once the Task completes. Thus in my example above this line

 await Task.Delay(1000);

causes the current context to be captured which is then used later on (after 1000 milliseconds here) to run the rest of the code which in this case is a simple return from the method. This “context” by default is the current SynchronizationContext  unless it is null in which case it is the current TaskScheduler.

GUI Apps

In the case of GUI and ASP.NET Applications the default SynchornizationContext permits only one chunk of code to run at a time. So if you think of this SynchronizationContext as a single pipe which will process all the instructions this is what happens.

1. This statement

delayTask.Wait();

causes the SynchornizationContext to wait for the Task in DelayAsync method to complete before any other code can execute.

2. Before the Task in DelayAsync method runs the SynchronizationContext is captured.

3. The Task in DelayAsync runs and effectively waits for 1000 milliseconds.

4. After 1000 milliseconds when the Task completes the await system tries to execute the remainder of the DelayAsync code on the captured SynchornizationContext. However this context is now already running that line of code (step 1.) which is waiting for the Task to complete. Thus await now has no way to run the remainder of DelayAsync method as the SynchornizationContext is waiting for the Task to complete and hence we have a deadlock!

So why not Console Apps?

Console applications believe it or not have a thread pool SynchornizationContext instead of “one piece of code at a time” SynchornizationContext. Thus in the case of a console app when the await completes it can schedule the remainder of the async (DelayAsync) method on a thread pool thread! Thus the DelayAsync method is able to complete which completes the Task and hence there is no deadlock.

 Finally – Async all the way

It is worth noting that this problem would perhaps never arise if the person writing the code followed this simple advice.

Do not mix Async code with Synchronous code. When using TPL Async programming techniques make sure you go Async all the way.

delayTask.Wait() is a synchronous blocking call whereas Task.Delay is not. Mixing and matching these two in the same execution context can lead to subtle yet potent problems.

This is especially true for applications which were written as synchronous applications but then slowly and gradually converted to adopt asynchronous programming techniques. In these applications as asynchronous code is introduced there are many potential places where situations like this can develop. Thus as you start replacing your synchronous code by asynchronous code ensure that you adopt the adage of “async all the way!”.

Leave a Comment

Your email address will not be published.

1 Trackback

  1. Nik's Corner – Deadlocks when using Tasks – solved (Pingback)