If you’re new to the async and await keywords in C# and .NET, then it’s likely you will eventually stumble across this deadlock scenario, which is difficult to debug if you’re not familiar with how async and await work under the hood.

Consider this example asynchronous method that fetches some text from a file:

public static async Task<string> GetTextAsync(string filePath)
{
    using (var stream = System.IO.File.OpenRead(filePath))
    {
        using (var reader = new System.IO.StreamReader(stream))
        {
            return await reader.ReadToEndAsync();
        }
    }
}

And a method that consumes this on the main UI thread, and sets the result to the text property of a UI control:

public void Button_Click()
{
    var task = GetTextAsync("/path/to/file.txt");
    
    var result = task.Result;

    this.SomeTextControl.Text = result;
}

The problem is that this code will create a deadlock, and the Button_Click() method will never complete, and the reason for this might not be immediately clear.

Task Parallel Library 101

To understand what’s going on, we need to understand a little more about the Task class that’s being passed back by the GetTextFromFile() method. A Task object, within .NET, represents an asynchronous piece of work: an operation that may take some time to complete. In most cases that work is going to be happening on a thread other than the main thread your application is executing on, which frees up your application to perform other tasks, such as updating the UI. The Task class is part of the Task Parallel Library, which greatly simplifies the way in which .NET programmers can write multithreaded code.

The async and await keywords introduced in C# 5.0 provide a handy short-cut to using the Task Parallel Library’s features with the minimum amount of additional code. I’ll talk about this in more detail in a future post.

Understanding the Problem

Our example Button_Click() method above tries to achieve the following steps:

  1. Create a Task variable. ‘task’, and call the GetTextAsync() method.
  2. Create a String variable ‘result’, and set it to the result of the task.
  3. Update a UI control with the text stored in ‘result’.

For simplicity, lets say there are two threads in our example:

  • Main Thread – Which is the main thread of execution, and where the Button_Click() method is called from.
  • Background Thread – Where the long-running task is executed.

The problem is that the code appears to halt at highlighted line 24, where we are trying to obtain the result of the task object. It’s important to understand that accessing the task.Result property will block the Main Thread if the task is not complete.

When the GetTextAsync() method is called, it’s also important to understand that the code within that method executes on the SAME thread it was called on (Main Thread), up until the first await keyword.

// Called from Main Thread ...
public static async Task<string> GetTextAsync(string filePath)
{
    // Main Thread
    using ( var stream = System.IO.File.OpenRead(filePath))
    {
        // Main Thread
        using (var reader = new System.IO.StreamReader(stream))
        {
            return await reader.ReadToEndAsync() // Background Thread;
        }
    }
}

When we hit the await keyword, the StreamReader’s .ReadToEndAsync() method executes on the Background Thread*. An important thing to understand here is that the ReadToEndAsync() method has been instructed to perform it’s task, and when complete return to the Main Thread to return it’s result.

At this point, while the Background Thread is running, execution of the GetTextAsync() code halts, and code execution is allowed to continue in the Button_Click() method, on the Main Thread.

This then flows into the var result = task.Result; line 24. Remember, accessing the task.Result property will halt the MainThread if the task has not completed it’s work … so it pauses the Main Thread and waits. So far, so good.

Meanwhile, the ReadToEndAsync() task completes it’s work on the Background Thread, and attempts to return the value on the Main Thread. The problem is that this thread is currently paused by the task.Result call.

And therefore we have a classic deadlock situation: A thread blocked waiting for a result; and a result that cannot be set because it is trying to return it’s value on the blocked thread.

The Solution

There are two ways of solving this problem. Your first option is to change the behaviour of the await call to ReadToEndAsync(). By default, the task ‘captures’ the current thread (or ‘context’), so it can attempt to pass it’s result back on that same thread. However, it’s possible to change that behaviour by using the .ConfigureAwait() method on the Task object like this:

public static async Task<string> GetTextAsync(string filePath)
{
    using (var stream = System.IO.File.OpenRead(filePath))
    {
        using (var reader = new System.IO.StreamReader(stream))
        {
            return await reader.ReadToEndAsync().ConfigureAwait(continueOnCapturedContext:false);
        }
    }
}

This instructs the task to NOT capture the current thread, and simply continue to the use the Background Thread it created  to return the result. This would allow the result to be set, even if the Main Thread was blocked.

The second solution, which may already be obvious to you, is that the code in this example is not being really executing asynchronously. Despite a Background Thread being created, and the long running work being performed on that thread; our example code still blocks the Main Thread while it waits for the result.

A better solution would be to make our initial method call asynchronous by simply adding async and await keywords to the Button_Click() method:

public async void Button_Click()
{
    var result = await GetTextAsync("/path/to/file.txt");

    this.SomeTextControl.Text = result;
}

As you can see, it’s simplified our code – and more importantly, it will no longer block our Main Thread, as a new thread is created when we call GetTextAsync().

Summary

I’d strongly recommend implementing both solutions in your code, as it will help guard against most deadlock scenarios.

As a general rule of thumb, if you’re using the async / await keywords, you should be careful using the Task.Result property, or Task.Wait() methods in your top-level methods.

One final thing that’s worth mentioning: In this somewhat simple example, I’ve assumed that the GetTextAsync() method takes some time to complete. In reality, it is possible for the task to complete before it is returned to the Button_Click() method. In this case, the code would appear to run as expected, as the task.Result property would have been set before we attempt to access it. This would lead to potentially dangerous code that might execute as expected on a fast computer, but caused deadlocks on a slower computer.

* There’s no assurance a new thread is actually being created here. Some IO-bound async methods simply release the current thread while the code waits for data to arrive, and resumes once it has data. The important point is that the calling thread is released, and allowed to continue executing code.


4 Comments

Ramon de Klein · 19 January 2017 at 10:38 am

I have created a deadlock detection library that can help you track down these issues a bit more easily (with minimal overhead and often non-intrusive). Check https://github.com/ramondeklein/deadlockdetection for more information.

Georgi Karavasilev · 29 October 2017 at 12:11 am

Huge Thanks! I was trying to fix this deadlock issue for hours and your explanation helped me!

Marc Wittke · 6 November 2017 at 2:05 pm

Really a nice article, helped me to get a bug sorted out.

JW Janse · 19 September 2018 at 7:07 pm

Thanks for the clear explanation. I had this deadlock issue today, where I was calling an async method from an MVC controller action, which itself is (and cannot be) made async. I solved this using the following approach found on StackOverflow, which has some performance overhead for creating a new Task:

var data = Task.Run(async () =>
await _mediator.Send(query)
).Result;

Leave a Reply

Avatar placeholder

Your email address will not be published.