When I use an async-await method (as the example below) in a HttpClient call, this code causes a deadlock. Replacing the async-await method with a t.ContinueWith
, it works properly. Why?
public class MyFilter: ActionFilterAttribute {
public override void OnActionExecuting(ActionExecutingContext filterContext) {
var user = _authService.GetUserAsync(username).Result;
}
}
public class AuthService: IAuthService {
public async Task<User> GetUserAsync (string username) {
var jsonUsr = await _httpClientWrp.GetStringAsync(url).ConfigureAwait(false);
return await JsonConvert.DeserializeObjectAsync<User>(jsonUsr);
}
}
This works:
public class HttpClientWrapper : IHttpClient {
public Task<string> GetStringAsync(string url) {
return _client.GetStringAsync(url).ContinueWith(t => {
_log.InfoFormat("Response: {0}", url);
return t.Result;
});
}
This code will deadlock:
public class HttpClientWrapper : IHttpClient {
public async Task<string> GetStringAsync(string url) {
string result = await _client.GetStringAsync(url);
_log.InfoFormat("Response: {0}", url);
return result;
}
}
Async methods are intended to be non-blocking operations. An await expression in an async method doesn't block the current thread while the awaited task is running. Instead, the expression signs up the rest of the method as a continuation and returns control to the caller of the async method.
The call to the async method starts an asynchronous task. However, because no Await operator is applied, the program continues without waiting for the task to complete. In most cases, that behavior isn't expected.
The biggest advantage of using async and await is, it is very simple and the asynchronous method looks very similar to a normal synchronous methods. It does not change programming structure like the old models (APM and EAP) and the resultant asynchronous method look similar to synchronous methods.
with async / await , you write less code and your code will be more maintainable than using the previous asynchronous programming methods such as using plain tasks. async / await is the newer replacement to BackgroundWorker , which has been used on windows forms desktop applications.
I describe this deadlock behavior on my blog and in a recent MSDN article.
await
will by default schedule its continuation to run inside the current SynchronizationContext
, or (if there is no SynchronizationContext
) the current TaskScheduler
. (Which in this case is the ASP.NET request SynchronizationContext
).SynchronizationContext
represents the request context, and ASP.NET only allows one thread in that context at a time.So, when the HTTP request completes, it attempts to enter the SynchronizationContext
to run InfoFormat
. However, there is already a thread in the SynchronizationContext
- the one blocked on Result
(waiting for the async
method to complete).
On the other hand, the default behavior for ContinueWith
by default will schedule its continuation to the current TaskScheduler
(which in this case is the thread pool TaskScheduler
).
As others have noted, it's best to use await
"all the way", i.e., don't block on async
code. Unfortunately, that's not an option in this case since MVC does not support asynchronous action filters (as a side note, please vote for this support here).
So, your options are to use ConfigureAwait(false)
or to just use synchronous methods. In this case, I recommend synchronous methods. ConfigureAwait(false)
only works if the Task
it's applied to has not already completed, so I recommend that once you use ConfigureAwait(false)
, you should use it for every await
in the method after that point (and in this case, in each method in the call stack). If ConfigureAwait(false)
is being used for efficiency reasons, then that's fine (because it's technically optional). In this case, ConfigureAwait(false)
would be necessary for correctness reasons, so IMO it creates a maintenance burden. Synchronous methods would be clearer.
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