Reputation: 7949
I have a server-side click event on a ASP.NET WebForms website. In this event I call a method which in turn calls its async partner method, adding .Wait()
on the call.
This method then goes several levels down (i.e., calls another async method, which calls another async method, and so on) and eventually calls an async method on an HttpClient object. At this point the thread seems to disappear down a rabbit hole; the method never calls back.
Now, I know that the async series of calls works as expected because the same code is also called from a Web API controller (the controller method calls the async version of that first method, not the synchronous 'partner' method), which is fully async, and it returns as expected.
So basically I have something like this, which never returns
protected void btn_Click(object sender, EventArgs e)
> Class1.DoSomething()
> Class1.DoSomethingAsync.Wait()
...
> await ClassN.Authenticate()
{
await myHttpClient.PostAsync() // never returns
}
I did try using .ConfigureAwait(false)
on that first async method but without any success.
I also have this, which does return
Task<IHttpActionResult> MyWebApiMethod()
> await Class1.DoSomethingAsync()
...
> await ClassN.Authenticate()
{
await myHttpClient.PostAsync() // does return
}
I've found that I can make the first version work if I change it to the following:
protected void btn_Click(object sender, EventArgs e)
> Class1.DoSomething()
> Task.Run(async () => await Class1.DoSomethingAsync()).Wait()
...
> await ClassN.Authenticate()
{
await myHttpClient.PostAsync()
}
But I don't know why.
Can anyone explain the difference between calling
Class1.DoSomethingAsync.Wait()
and calling
Task.Run(async () => await Class1.DoSomethingAsync()).Wait()
Upvotes: 3
Views: 5324
Reputation: 456417
I explain this behavior in my blog post Don't Block on Asynchronous Code and my MSDN article on asynchronous best practices.
the thread seems to disappear down a rabbit hole; the method never calls back.
This happens because one of the await
s is attempting to resume on the ASP.NET context, but the request thread (using that ASP.NET context) is blocked waiting for the task to complete. This is what causes your deadlock.
I did try using .ConfigureAwait(false) on that first async method but without any success.
In order to avoid this deadlock using ConfigureAwait(false)
, it must be applied to every await
in every method called. So DoSomethingAsync
must use it for every await
, every method that DoSomethingAsync
calls must use it for every await
(e.g., Authenticate
), every method that those methods call must use it for every await
(e.g., PostAsync
), etc. Note that at the end here, you are dependent on library code, and in fact HttpClient
has missed a few of these in the past.
I've found that I can make the first version work if I change it [to use
Task.Run
].
Yup. Task.Run
will execute its delegate on a thread pool thread without any context. So this is why there's no deadlock: none of the await
s attempt to resume on the ASP.NET context.
Why don't you use it the correct way. async void btn_Click and await Class1.DoSomethingAsync() ?
Do not use Task.Run with already asynchronous methods, this is waste of threads. Just change signature of button_click eventhandler
And here's the right answer: don't block on asynchronous code. Just use an async void
event handler and use await
instead.
P.S. ASP.NET Core no longer has an ASP.NET context, so you can block as much as you want without fear of deadlocks. But you still shouldn't, of course, because it's inefficient.
Upvotes: 8