An Async/Await Example That Causes a Deadlock

An async/await example that causes a deadlock

Take a look at this example, Stephen has a clear answer for you:

So this is what happens, starting with the top-level method (Button1_Click for UI / MyController.Get for ASP.NET):

  1. The top-level method calls GetJsonAsync (within the UI/ASP.NET context).

  2. GetJsonAsync starts the REST request by calling HttpClient.GetStringAsync (still within the context).

  3. GetStringAsync returns an uncompleted Task, indicating the REST request is not complete.

  4. GetJsonAsync awaits the Task returned by GetStringAsync. The context is captured and will be used to continue running the GetJsonAsync method later. GetJsonAsync returns an uncompleted Task, indicating that the GetJsonAsync method is not complete.

  5. The top-level method synchronously blocks on the Task returned by GetJsonAsync. This blocks the context thread.

  6. ... Eventually, the REST request will complete. This completes the Task that was returned by GetStringAsync.

  7. The continuation for GetJsonAsync is now ready to run, and it waits for the context to be available so it can execute in the context.

  8. Deadlock. The top-level method is blocking the context thread, waiting for GetJsonAsync to complete, and GetJsonAsync is waiting for the context to be free so it can complete. For the UI example, the "context" is the UI context; for the ASP.NET example, the "context" is the ASP.NET request context. This type of deadlock can be caused for either "context".

Another link you should read: Await, and UI, and deadlocks! Oh my!

Why does this async/await code NOT cause a deadlock?

await returns to the original synchronization context, whether that is the UI thread (in desktop UI applications) or the request context in ASP.NET (not core).

In a GUI application, you'd have a deadlock because the UI thread was locked by .Result. await would await forever for this call to finish.

Console applications and ASP.NET Core have no synchronization context, so calling .Result won't cause a deadlock.

PS for VS 15.3:

Visual Studio 2017 15.3 Preview 2 (gasp) allows asynchronous main applications. With it, You can write :

public static Task Main()
{
var length = await GetPageLengthAsync("http://csharpindepth.com");
Console.WriteLine(length);
}

Why does async await pattern in this console app caused a deadlock?

You never start your task in DoNothing.

https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?view=netframework-4.8#remarks

Separating task creation and execution

The Task class also provides constructors that initialize the task but that do not schedule it for execution. For performance reasons, the Task.Run or TaskFactory.StartNew method is the preferred mechanism for creating and scheduling computational tasks, but for scenarios where creation and scheduling must be separated, you can use the constructors and then call the Task.Start method to schedule the task for execution at a later time.

async/await deadlock caused by Thread.Sleep?

I see a couple red flags:

First, change Thread.Sleep(1000) to await Task.Delay(1000) so you're not blocking the thread. That may be part of your problem, although I don't see quite how at the moment.

Second, your Main method:

static void Main(string[] args)
{
Log();
Console.ReadKey();
}

There are two issues:

  1. Log() returns a Task that you are not observing to see when it has completed. You are probably getting a compiler warning about that.
  2. Your Main method is void, which means it can't use await.

The reason these are a problem is that the await keyword will return when it acts on an incomplete Task (and the rest of the method is signed up as the "continuation" to happen when after the waiting is done). Usually it returns its own Task object, but if it can't because the method is void, it returns nothing. But it still returns.

Because you're not doing anything with the Task returned from Log(), your Main method continues and you never know if it completes.

I see you are using Console.ReadKey() to keep the program running, which is "ok". But you could just change your Main method to async Task and await Log():

static async Task Main(string[] args)
{
await Log();
}

That is assuming you are using C# 7.1+, since that's when you could start returning a Task from Main().

Also, you are creating 100,000 threads but only allowing one to work at any one time. Why not just create a regular for or foreach loop and do them one at a time?

for (var i = 0; i < 100000; i++)
{
await logger.Log(i, new LogMessage() { Message = oneKBMessage, TimeStamp = DateTime.Now });
}

Then you don't even need the locks - at least for this specific use case. If there are other cases where you will call it from different threads, then keep the locks.

How can blocking async calls cause a deadlock if async calls aren't necessarily executed on a different thread?

What I'm asking is, where is GetJsonAsync executing when it's called from Button1_Click if this call doesn't create a new thread for it to execute on. Before the await within GetJsonAsync, isn't it executing the Console.WriteLine(...) within the UI context still?

I recommend reading my async intro. In summary:

Every asynchronous method begins executing synchronously. This code:

public void Button1_Click(...)
{
var jsonTask = GetJsonAsync(...);
textBox1.Text = jsonTask.Result;
}

calls GetJsonAsync on the UI thread, and it does begin executing on the UI thread. It executes Console.WriteLine on the UI thread, news up a client on the UI thread, and even calls GetStringAsync on the UI thread. It gets a task back from that method, and then awaits it (I'm ignoring the ConfigureAwait(true) for simplicity).

The await is the point at which things may become asynchronous. The task isn't complete (i.e., the client hasn't received the string yet), so GetJsonAsync returns an incomplete task to its caller. Then Button1_Click blocks the UI thread, waiting for that task to complete (by calling .Result).

So, the state is currently GetJsonAsync is no longer running on the UI thread. It is not actually "running" anywhere.

Later, when that string result arrives, the task that was returned from GetStringAsync is completed, and GetJsonAsync needs to resume executing. It's not already on the UI thread; it's not anywhere at the moment. Since the await captured a UI context, it will attempt to resume on that context (on the UI thread).

Deadlock while using async/await

What's blocking is not the task itself, it's the call to Result. A Task represents an asynchronous operation, but calling its Result property, or calling Wait() will block the current thread until the method returns. And in a lot of cases, it will cause a deadlock because the task is not able to complete with it's calling thread blocked!

To prevent that, chain the tasks asynchronously, using async and await

private async void button1_Click(object sender, EventArgs e)
{
var result = await HeavyWorkAsync(); // <=== await
richTextBox1.AppendText(result);
}

Also, Task.Delay(10).Wait(); completely defeats the prupose of using tasks in the first place: that will block the current thread. If that's really what you want to do (and it's pretty unlikely), call Thread.Sleep(10);instead, it will make your intent much clearer, and you will have less hoops to jump through. Or better, use await Task.Delay(10);in an async method.

About ConfigureAwait

What exactly does ConfigureAwait(false) do?

It removes the obligation for the continuation of the task to run in the same context as the caller of the task. In most cases that means that the continuation is no longer guaranteed to run on the same context. So if I have a method thad does Foo(), waits a little then Bar() like this one:

async Task DoStufAsync()
{
Foo();
await Task.Delay(10);
Bar(); // run in the same context as Foo()
}

I'm guaranteed Bar will run in the same context. If I had ConfigureAwait(false), it's no longer the case

async Task DoStufAsync()
{
Foo();
await Task.Delay(10).ConfigureAwait(false);
Bar(); // can run on another thread as Foo()
}

When you're using ConfigureAwait(false), you tell your program you dont mind about the context. It can solve some deadlocking problems, but isn't usually the right solution. The right solution is most likely never to wait for tasks in a blocking way, and being asynchronous all the way.

Why this async method don't cause deadlock on a thread?

You don't experience a deadlock in the first code example because of the SynchronizationContext. This context says what the await must do to restore your code. When you start a new task (Task.Run), you will get the default context. In the button1_click you get the context from the form.

The context for the form allows to execute only a single thread (the one used to paint/update your form).

When you call .Wait(), then you keep that thread 'locked' with waiting until the task is done. That means that the thread is not released for another job and this is one of the reasons that the form goes into the 'not responding' state.

When you do an await [code], and that code is done, then it will ask the synchronization context to schedule the remainder of the code.

  • In case of the default context, any free thread is taken and your code will continue, the task is marked as done, and thus the 'loop' in the .Wait() gets signaled that the task is done and continues.
  • In case of the forms context, there is only a single thread, thus the completion of this task is scheduled to run after the current code is done. This is the reason of the deadlock.

Avoiding this deadlock is the main reason that you most often get the suggestion to use ConfigureAwait(false), this tells the await that it should substitute the current synchronization context with the default one, thus allowing you to use any free thread.
The only reason you don't want to use this (or say ConfigureAwait(true)) is if you need to update something on your form (WPF or WinForms) or you need your http context (ASP.NET, not ASP.NET Core). This is because only a single thread has access to your form or http context. Here you don't get an exception on your MessageBox.Show because this is 'outside' of the forms context and doesn't need this special thread/synchronization context.

See https://devblogs.microsoft.com/dotnet/configureawait-faq/ for more information.



Related Topics



Leave a reply



Submit