I am learning async/await and after I read this article Don't Block on Async Code
and this Is async/await suitable for methods that are both IO and CPU bound
I notice one Tip from @Stephen Cleary 's article.
Using ConfigureAwait(false) to avoid deadlocks is a dangerous practice. You would have to use ConfigureAwait(false) for every await in the transitive closure of all methods called by the blocking code, including all third- and second-party code. Using ConfigureAwait(false) to avoid deadlock is at best just a hack).
It appeared again in the code of the post as I have attached above.
public async Task<HtmlDocument> LoadPage(Uri address)
{
using (var httpResponse = await new HttpClient().GetAsync(address)
.ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
using (var responseContent = httpResponse.Content)
using (var contentStream = await responseContent.ReadAsStreamAsync()
.ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
return LoadHtmlDocument(contentStream); //CPU-bound
}
As my knowledge when we using ConfigureAwait(false) the rest of async method will be run in the thread pool. Why we need to add it into every await in transitive closure? I myself just think this is the correct version as what I knew.
public async Task<HtmlDocument> LoadPage(Uri address)
{
using (var httpResponse = await new HttpClient().GetAsync(address)
.ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
using (var responseContent = httpResponse.Content)
using (var contentStream = await responseContent.ReadAsStreamAsync()) //IO-bound
return LoadHtmlDocument(contentStream); //CPU-bound
}
It means the second use of ConfigureAwait(false) in using block is useless. Please tell me the correct way. Thanks in advance.
Close, but there is an important caveat you are missing. When you resume after awaiting a task with
ConfigureAwait(false)
, you will resume on an arbitrary thread. Take note of the words "when you resume."Let me show you something:
Consider the
await
inExample1
. Although you are awaiting anasync
method, that method does not actually perform any asynchronous work. If anasync
method doesn'tawait
anything, it executes synchronously, and the awaiter never resumes because it never suspended in the first place. As this example shows, calls toConfigureAwait(false)
may be superfluous: they may have no effect at all. In this example, whatever context you were on when you enterExample1
is the context you will be on after theawait
.Not quite what you expected, right? And yet, it's not altogether unusual. Many
async
methods may contain fast paths that don't require the caller to suspend. The availability of a cached resource is a good example (thanks, @jakub-dąbek!), but there plenty of other reasons anasync
method might bail early. We often check for various conditions at the beginning of a method to see if we can avoid doing unnecessary work, andasync
methods are no different.Let's look at another example, this time from a WPF application:
Take a look at
Example2
. The first method weawait
always runs asynchronously. By the time we hit the secondawait
, we know we're running on a thread pool thread, so there's no need forConfigureAwait(false)
on the second call, right? Wrong. Despite havingAsync
in the name and returning aTask
, our second method wasn't written usingasync
andawait
. Instead, it performs its own scheduling and uses aTaskCompletionSource
to communicate the result. When you resume from yourawait
, you might[1] end up running on whatever thread provided the result, which in this case is WPF's dispatcher thread. Whoops.The key takeaway here is that you often don't know exactly what an 'awaitable' method does. With or without
CongifureAwait
, you might end up running somewhere unexpected. This can happen at any level of anasync
call stack, so the surest way to avoid inadvertently taking ownership of a single-threaded context is to useConfigureAwait(false)
with everyawait
, i.e., throughout the transitive closure.Of course, there may be times when you want to resume on your current context, and that's fine. That is ostensibly why it's the default behavior. But if you don't genuinely need it, then I recommend using
ConfigureAwait(false)
by default. This is especially true for library code. Library code can get called from anywhere, so it's best adhere to the principle of least surprise. That means not locking other threads out of your caller's context when you don't need it. Even if you useConfigureAwait(false)
everywhere in your library code, your caller will still have the option to resume on their original context if that's what they want.[1] This behavior may vary by framework and compiler version.