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.
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(); } }
The HTTP request is sent out, and HttpClient. GetAsync returns an uncompleted Task .
Using SendAsync, we can write the code as: static async Task SendURI(Uri u, HttpContent c) { var response = string. Empty; using (var client = new HttpClient()) { HttpRequestMessage request = new HttpRequestMessage { Method = HttpMethod. Post, RequestUri = u, Content = c }; HttpResponseMessage result = await client.
GetAsync(String, HttpCompletionOption) Send a GET request to the specified Uri with an HTTP completion option as an asynchronous operation. GetAsync(String, CancellationToken) Send a GET request to the specified Uri with a cancellation token as an asynchronous operation.
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).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.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
.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:
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);
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:
async
/await
intro post, which includes a brief description of how Task
awaiters use SynchronizationContext
.SynchronizationContext
restricts the request context to just one thread at a time.Update 2012-07-13: Incorporated this answer into a blog post.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With