ASP.Net–Do not use Task .Result in main context

Overview

You can guarantee you will deadlock if you have a call similar to this in your code, where CallHttp is a an AsyncTask that awaits a result:

 public class DeadlockController : ApiController
   {
       public string Get() {
           string ret = "";

           // deadlock
           ret = Utilities.CallHttp().Result;

           return ret;
       }

Cause and Symptoms

The reason why is that the ASP.Net context is being used to run this call and the call inside of CallHttp().  The inner function has an await and tries to acquire the ASP.Net context which in turn is waiting for the completion of the Result call. This same thing can happen in the UI Context of a Winforms app (See: https://blogs.msdn.microsoft.com/pfxteam/2011/01/13/await-and-ui-and-deadlocks-oh-my/ ).  You will notice that calls do not complete and/or the process starts to leak threads and other resources.

Confirming issue

You can have the customer look for code that is calling async methods improperly.  If you take a dump and run the Debug Diagnostics it will show something similar to this:

The following threads in issues.iisexpress.dmp are waiting in System.Threading.Monitor.Wait
( 32 33 34 38 39 40 42 )
14.58% of threads blocked (7 threads)

Threads waiting in Monitor.Wait are actually waiting to re-acquire a lock which they released. The signal to reacquire the lock will be given by a call to Monitor.Pulse or Monitor.PulseAll or when the timeout is hit.
Look at the callstack of the thread to see which function is making a call to Monitor.Wait and then review code to find out the function which is supposed to call Monitor.Pulse or Monitor.PulseAll and see why that function is not getting called.

 

If you attach to a debug version of your app and break in the debugger you will see many threads waiting on the .Return function to complete.

 

Here is a sample stack (from Debug Diagnostics) where you see the call deadlocked:

 

.NET Call Stack

[[GCFrame]]

[[HelperMethodFrame_1OBJ] (System.Threading.Monitor.ObjWait)] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object)

mscorlib_ni!System.Threading.Monitor.Wait(System.Object, Int32, Boolean)+17

mscorlib_ni!System.Threading.Monitor.Wait(System.Object, Int32)+c

mscorlib_ni!System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken)+317

mscorlib_ni!System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken)+9c

mscorlib_ni!System.Threading.Tasks.Task.InternalWait(Int32, System.Threading.CancellationToken)+155

mscorlib_ni!System.Threading.Tasks.Task`1[[System.__Canon, mscorlib]].GetResultCore(Boolean)+4b

mscorlib_ni!System.Threading.Tasks.Task`1[[System.__Canon, mscorlib]].get_Result()+22

WaitVsResult.Controllers.DeadlockController.Get()+50

 

Fix

Do not block by using the .Result property of an asynchronous call.  Patters such as marking the function async, or the aspx page as async and calling with await will fix this issue.  You can also use Task.Run in a lambda and call .Wait to synchronize the call.

Good code:

       public async Task<string> Get() {
           string ret = "";

           // does NOT deadlock
           ret = await Utilities.CallHttp();

           return ret;
       }

       public string Get(int id) {
           string ret = "";

           // Start a task - calling an async function in this example
           Task<string> callTask = Task.Run(() => Utilities.CallHttp());
           // Wait for it to finish
           callTask.Wait();
           // Get the result
           ret = callTask.Result;

           return ret;
       }

 

Note

You will also see a great explanation here: https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

Comments

  • Anonymous
    August 28, 2017
    What are peoples' thoughts on HostingEnvironment.QueueBackgroundWorkItem? I've been using this successfully for quite some time now.