You are misusing the API.
Here’s the situation: in ASP.NET, only one thread can handle a request at a time. You can do some parallel processing if necessary (borrowing additional threads from the thread pool), but only one thread would have the request context (the additional threads do not have the request context).
This is managed by the ASP.NET SynchronizationContext.
By default, when you await a Task, the method resumes on a captured SynchronizationContext (or a captured TaskScheduler, if there is no SynchronizationContext). Normally, this is just what you want: an asynchronous controller action will await something, and when it resumes, it resumes with the request context.
So, here’s why test5 fails:
Test5Controller.GetexecutesAsyncAwait_GetSomeDataAsync(within the ASP.NET request context).AsyncAwait_GetSomeDataAsyncexecutesHttpClient.GetAsync(within the ASP.NET request context).- The HTTP request is sent out, and
HttpClient.GetAsyncreturns an uncompletedTask. AsyncAwait_GetSomeDataAsyncawaits theTask; since it is not complete,AsyncAwait_GetSomeDataAsyncreturns an uncompletedTask.Test5Controller.Getblocks the current thread until thatTaskcompletes.- The HTTP response comes in, and the
Taskreturned byHttpClient.GetAsyncis completed. AsyncAwait_GetSomeDataAsyncattempts to resume within the ASP.NET request context. However, there is already a thread in that context: the thread blocked inTest5Controller.Get.- Deadlock.
Here’s why the other ones work:
- (
test1,test2, andtest3):Continuations_GetSomeDataAsyncschedules the continuation to the thread pool, outside the ASP.NET request context. This allows theTaskreturned byContinuations_GetSomeDataAsyncto complete without having to re-enter the request context. - (
test4andtest6): Since theTaskis awaited, the ASP.NET request thread is not blocked. This allowsAsyncAwait_GetSomeDataAsyncto use the ASP.NET request context when it is ready to continue.
And here’s the best practices:
- In your “library”
asyncmethods, useConfigureAwait(false)whenever possible. In your case, this would changeAsyncAwait_GetSomeDataAsyncto bevar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); - Don’t block on
Tasks; it’sasyncall the way down. In other words, useawaitinstead ofGetResult(Task.ResultandTask.Waitshould also be replaced withawait).
That way, you get both benefits: the continuation (the remainder of the AsyncAwait_GetSomeDataAsync method) is run on a basic thread pool thread that doesn’t have to enter the ASP.NET request context; and the controller itself is async (which doesn’t block a request thread).
More information:
- My
async/awaitintro post, which includes a brief description of howTaskawaiters useSynchronizationContext. - The Async/Await FAQ, which goes into more detail on the contexts. Also see Await, and UI, and deadlocks! Oh, my! which does apply here even though you’re in ASP.NET rather than a UI, because the ASP.NET
SynchronizationContextrestricts the request context to just one thread at a time. - This MSDN forum post.
- Stephen Toub demos this deadlock (using a UI), and so does Lucian Wischik.
Update 2012-07-13: Incorporated this answer into a blog post.