Why doesn't this call to .Result provoke a deadlock?

Multi tool use
Why doesn't this call to .Result provoke a deadlock?
I'm still trying to wrap my head around when I can use .Result, and when I can't. I have been trying to avoid it entirely, by using async "all the way down".
While doing some work in our ASP.NET web app (NOT core), I came across this code. Now, the code works (and has been working for 2 years), so I know it works, I just don't know why it works.
This is fired from a sync controller method. It is a chain of method calls that goes through a few layers until it finally does some HTTP work. I have simplified the code here:
// Class1
public byte GetDocument1(string url)
{
return class2.GetDocument2(url).Result;
}
// Class2
public Task<byte> GetDocument2(string url)
{
return class3.GetDocument3(url)
}
// Class3
public async Task<byte> GetDocument3(string url)
{
var client = GetHttpClient(url);
var resp = client.GetAsync(url).Result;
if(resp.StatusCode == HttpStatusCode.OK)
{
using(var httpStream = await resp.Content.ReadAsStreamAsync())
{
// we are also using
await httpStream.ReadAsync(...);
}
}
}
So as far as I can tell, when this all starts, I'm on the "main ASP sync context" (I start in a controller and eventually get to this code). We aren't using any .ConfigureAwait(false)
, so I believe we are always returning to this context.
.ConfigureAwait(false)
GetDocument3
client.GetAsync(url).Result
GetDocument3
.Result
await
.Result
GetDocument1
.Result
I'm still trying to wrap my head around when I can use .Result
@mason - c'mon man, you know that's just not always possible. In addition, I HATE not knowing stuff. .Result is real, and apparently legit, and I feel I should know how/when to use it.
– emery.noel
Jul 2 at 14:24
What do you mean "not always possible"? If you aren't in an async context, don't try to use async code. Which means you should always be able to
await
and should never need to use .Result
. If you want to know when to use it, the answer is "never".– mason
Jul 2 at 14:33
await
.Result
If the code you're invoking doesn't rely on the synchronization context, than it will not deadlock.
– Paulo Morgado
Jul 2 at 14:44
1 Answer
1
client.GetAsync(url).Result
is synchronously blocking. resp.Content.ReadAsStreamAsync()
is actually not doing any awaiting. The Task
is already completed, because HttpCompletionOption.ResponseContentRead
is used in GetAsync
, so the entire block of code here is synchronous code pretending to be asynchronous.
client.GetAsync(url).Result
resp.Content.ReadAsStreamAsync()
Task
HttpCompletionOption.ResponseContentRead
GetAsync
In general, you should never use .Result
or .Wait
or Task.Wait*
- if you absolutely have to, use GetAwaiter().GetResult()
, which doesn't throw exceptions wrapped in AggregateException
s, for which you may not have a catch
, but even that should be avoided like the plague. You're right to use async
all the way down.
.Result
.Wait
Task.Wait*
GetAwaiter().GetResult()
AggregateException
catch
async
The deadlock is only a problem in a context that wants to return back to the original thread (e.g. UI or ASP.NET), and that thread is blocked by a synchronous wait. If you always use ConfigureAwait(false)
on every await
, unless you know you actually need to preserve the context on the continuation (usually only at the top level UI event handler because you need to update something on the UI, or in the top level of the ASP.NET controller) then you should be safe.
ConfigureAwait(false)
await
Synchronous blocking inside asynchronous code also isn't good, but won't cause the deadlock. Using await
with ConfigureAwait(true)
(the default), inside a synchronous wait, in a context that wants to return back to a specific thread will cause the deadlock.
await
ConfigureAwait(true)
@georgeHeylar - re: 3rd paragraph. Ok but, I'm in ASP.NET. I'm still on that original context/thread. I am synchronously blocking on that
GetAsync().Result
, right? So if I'm blocking, and GetAsync()
is going to continue on that blocked thread ... how does that work? Is it "ok" to use .Result
BECAUSE GetAsync()
is guaranteed to run on some other thread, or something like that?– emery.noel
Jul 2 at 14:31
GetAsync().Result
GetAsync()
.Result
GetAsync()
Actually,
GetAwaiter().GetResult()
unwraps the AggregateException
.– Paulo Morgado
Jul 2 at 14:45
GetAwaiter().GetResult()
AggregateException
When you use
.Result
, it executes synchronously, blocking the thread and never leaving it, so it has no problem getting back on that thread because it never left. If you were using .Result
on an outer Task
and an await
with .ConfigureAwait(true)
on the inner Task
in asp.net, the inner wouldn't be able to return to the thread that was currently blocking on the outer Task
. Thanks Paulo, I didn't know that! Just not getting an unexpected AggregateException
that probably has no catch
is really what I care about when using it.– George Helyar
Jul 2 at 17:13
.Result
.Result
Task
await
.ConfigureAwait(true)
Task
Task
AggregateException
catch
Thank you @GeorgeHeylar, it is becoming clearer!
– emery.noel
Jul 5 at 10:45
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
I'm still trying to wrap my head around when I can use .Result
why? Why not just follow the advice: don't use it.– mason
Jul 2 at 12:22