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.C
Your first line:
var user = _authService.GetUserAsync(username).Result;
blocks that thread and the current context while it waits for the result of GetUserAsync
.
When using await
it attempts to run any remaining statements back on the original context after the task being waited on finishes, which causes deadlocks if the original context is blocked (which is is because of the .Result
). It looks like you attempted to preempt this problem by using .ConfigureAwait(false)
in GetUserAsync
, however by the time that that await
is in effect it's too late because another await
is encountered first. The actual execution path looks like this:
_authService.GetUserAsync(username)
_httpClientWrp.GetStringAsync(url) // not actually awaiting yet (it needs a result before it can be awaited)
await _client.GetStringAsync(url) // here's the first await that takes effect
When _client.GetStringAsync
finishes, the rest of the code can't continue on the original context because that context is blocked.
ContinueWith
doesn't try to run the other block on the original context (unless you tell it to with an additional parameter) and thus does not suffer from this problem.
This is the difference in behavior that you noticed.
If you still want to use async
instead of ContinueWith
, you can add the .ConfigureAwait(false)
to the first encountered async
:
string result = await _client.GetStringAsync(url).ConfigureAwait(false);
which as you most likely already know, tells await
not to try to run the remaining code on the original context.
Whenever possible, attempt to not use blocking methods when using async/await. See Preventing a deadlock when calling an async method without using await for avoiding this in the future.
I found the other solutions posted here did not work for me on ASP .NET MVC 5, which still uses synchronous Action Filters. The posted solutions don't guarantee a new thread will be used, they just specify that the same thread does not HAVE to be used.
My solution is to use Task.Factory.StartNew() and specifying TaskCreationOptions.LongRunning in the method call. This ensures a new/different thread is always used, so you can be assured you will never get a deadlock.
So, using the OP example, the following is the solution that works for me:
public class MyFilter: ActionFilterAttribute {
public override void OnActionExecuting(ActionExecutingContext filterContext) {
// var user = _authService.GetUserAsync(username).Result;
// Guarantees a new/different thread will be used to make the enclosed action
// avoiding deadlocking the calling thread
var user = Task.Factory.StartNew(
() => _authService.GetUserAsync(username).Result,
TaskCreationOptions.LongRunning).Result;
}
}
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.
Granted, my answer is only partial, but I'll go ahead with it anyway.
Your Task.ContinueWith(...)
call does not specify the scheduler, therefore TaskScheduler.Current
will be used - whatever that is at the time. Your await
snippet, however, will run on the captured context when the awaited task completes, so the two bits of code may or may not produce similar behaviour - depending on the value of TaskScheduler.Current
.
If, say, your first snippet is called from the UI code directly (in which case TaskScheduler.Current == TaskScheduler.Default
, the continuation (logging code) will execute on the default TaskScheduler
- that is, on the thread pool.
In the second snippet, however, the continuation (logging) will actually run on the UI thread regardless of whether you use ConfigureAwait(false)
on the task returned by GetStringAsync, or not. ConfigureAwait(false)
will only affect the execution of the code after the call to GetStringAsync
is awaited.
Here's something else to illustrate this:
private async void Form1_Load(object sender, EventArgs e)
{
await this.Blah().ConfigureAwait(false);
// InvalidOperationException here.
this.Text = "Oh noes, I'm no longer on the UI thread.";
}
private async Task Blah()
{
await Task.Delay(1000);
this.Text = "Hi, I'm on the UI thread.";
}
The given code sets the Text within Blah() just fine, but it throws a cross-threading exception inside the continuation in the Load handler.