HttpClient.GetAsync(...) never returns when using await/async
.net, async-await, asynchronous, c#, dotnet-httpclient
Solution
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.Get` executes `AsyncAwait_GetSomeDataAsync` (within the ASP.NET request context).
- `AsyncAwait_GetSomeDataAsync` executes `HttpClient.GetAsync` (within the ASP.NET request context).
- The HTTP request is sent out, and `HttpClient.GetAsync` returns an uncompleted `Task`.
- `AsyncAwait_GetSomeDataAsync` awaits the `Task`; since it is not complete, `AsyncAwait_GetSomeDataAsync` returns an uncompleted `Task`.
- `Test5Controller.Get` blocks the current thread until that `Task` completes.
- The HTTP response comes in, and the `Task` returned by `HttpClient.GetAsync` is completed.
- `AsyncAwait_GetSomeDataAsync` attempts to resume within the ASP.NET request context. However, there is already a thread in that context: the thread blocked in `Test5Controller.Get`.
- Deadlock.
Here's why the other ones work:
- (`test1`, `test2`, and `test3`): `Continuations_GetSomeDataAsync` schedules the continuation to the thread pool, outside the ASP.NET request context. This allows the `Task` returned by `Continuations_GetSomeDataAsync` to complete without having to re-enter the request context.
- (`test4` and `test6`): Since the `Task` is awaited, the ASP.NET request thread is not blocked. This allows `AsyncAwait_GetSomeDataAsync` to use the ASP.NET request context when it is ready to continue.
And here's the best practices:
- In your "library" `async` methods, use `ConfigureAwait(false)` whenever possible. In your case, this would change `AsyncAwait_GetSomeDataAsync` to be `var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);`
- Don't block on `Task`s; it's `async` all the way down. In other words, use `await` instead of `GetResult` (`Task.Result` and `Task.Wait` should also be replaced with `await`).
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`/`await` intro post, which includes a brief description of how `Task` awaiters use `SynchronizationContext`.
- 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 `SynchronizationContext` restricts 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.
Problem
Edit: This question looks like it might be the same problem, but has no responses... Edit: In test case 5 the task appears to be stuck in `WaitingForActivation` state. I've encountered some odd behaviour using the System.Net.Http.HttpClient in .NET 4.5 - where "awaiting" the result of a call to (e.g.) `httpClient.GetAsync(...)` will never return. This only occurs in certain circumstances when using the new async/await language functionality and Tasks API - the code always seems to work when using only continuations. Here's some code which reproduces the problem - drop this into a new "MVC 4 WebApi project" in Visual Studio 11 to expose the following GET endpoints: ``` /api/test1 /api/test2 /api/test3 /api/test4 /api/test5 <--- never completes /api/test6 ``` Each of the endpoints here return the same data (the response headers from stackoverflow.com) except for `/api/test5` which never completes. Have I encountered a bug in the HttpClient class, or am I misusing the API in some way? Code to reproduce: ``` public class BaseApiController : ApiController { /// <summary> /// Retrieves data using continuations /// </summary> protected Task<string> Continuations_GetSomeDataAsync() { var httpClient = new HttpClient(); var t = httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead); return t.ContinueWith(t1 => t1.Result.Content.Headers.ToString()); } /// <summary> /// Retrieves data using async/await /// </summary> protected async Task<string> AsyncAwait_GetSomeDataAsync() { var httpClient = new HttpClient(); var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead); return result.Content.Headers.ToString(); } } public class Test1Controller : BaseApiController { /// <summary> /// Handles task using Async/Await /// </summary> public async Task<string> Get() { var data = await Continuations_GetSomeDataAsync(); return data; } } public class Test2Controller : BaseApiController { /// <summary> /// Handles task by blocking the thread until the task completes /// </summary> public string Get() { var task = Continuations_GetSomeDataAsync(); var data = task.GetAwaiter().GetResult(); return data; } } public class Test3Controller : BaseApiController { /// <summary> /// Passes the task back to the controller host /// </summary> public Task<string> Get() { return Continuations_GetSomeDataAsync(); } } public class Test4Controller : BaseApiController { /// <summary> /// Handles task using Async/Await /// </summary> public async Task<string> Get() { var data = await AsyncAwait_GetSomeDataAsync(); return data; } } public class Test5Controller : BaseApiController { /// <summary> /// Handles task by blocking the thread until the task completes /// </summary> public string Get() { var task = AsyncAwait_GetSomeDataAsync(); var data = task.GetAwaiter().GetResult(); return data; } } public class Test6Controller : BaseApiController { /// <summary> /// Passes the task back to the controller host /// </summary> public Task<string> Get() { return AsyncAwait_GetSomeDataAsync(); } } ```