Why do i need to use ConfigureAwait(false) in all of transitive closure?
Asked Answered
S

1

36

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.

Stlaurent answered 7/9, 2017 at 10:32 Comment(0)
S
51

As my knowledge when we using ConfigureAwait(false) the rest of async method will be run in the thread pool.

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:

public async Task<string> GetValueAsync()
{
    return "Cached Value";
}

public async Task Example1()
{
    await this.GetValueAsync().ConfigureAwait(false);
}

Consider the await in Example1. Although you are awaiting an async method, that method does not actually perform any asynchronous work. If an async method doesn't await anything, it executes synchronously, and the awaiter never resumes because it never suspended in the first place. As this example shows, calls to ConfigureAwait(false) may be superfluous: they may have no effect at all. In this example, whatever context you were on when you enter Example1 is the context you will be on after the await.

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 an async 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, and async methods are no different.

Let's look at another example, this time from a WPF application:

async Task DoSomethingBenignAsync()
{
    await Task.Yield();
}

Task DoSomethingUnexpectedAsync()
{
    var tcs = new TaskCompletionSource<string>();
    Dispatcher.BeginInvoke(Action(() => tcs.SetResult("Done!")));
    return tcs.Task;
}

async Task Example2()
{
    await DoSomethingBenignAsync().ConfigureAwait(false);
    await DoSomethingUnexpectedAsync();
}

Take a look at Example2. The first method we await always runs asynchronously. By the time we hit the second await, we know we're running on a thread pool thread, so there's no need for ConfigureAwait(false) on the second call, right? Wrong. Despite having Async in the name and returning a Task, our second method wasn't written using async and await. Instead, it performs its own scheduling and uses a TaskCompletionSource to communicate the result. When you resume from your await, 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 ConfigureAwait, you might end up running somewhere unexpected. This can happen at any level of an async call stack, so the surest way to avoid inadvertently taking ownership of a single-threaded context is to use ConfigureAwait(false) with every await, 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 use ConfigureAwait(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.

Specie answered 7/9, 2017 at 10:53 Comment(7)
Also there is a possibility that the call returns synchronously with the data (maybe it was cached). In that situation the ConfigureAwait doesn't do anything and subsequent calls have to use itFarming
@JakubDąbek Leave it to me to omit the single most obvious reason ;). Thank you.Specie
After I read your answer carefully and seriously, now I better understand about the behavior of async/await ConfigureAwait. I just want to leave this link which I also read carefully again after received your answer Async/Await - Best Practices in Asynchronous Programming. In the link have the section Figure 6 Handling a Returned Task that Completes Before It’s Awaited which mentioned same as what you said. Thanks.Stlaurent
What if we at Example1 make return await Task.FromResult("Cached Value"); does it make it async? There is an await, but no task started?Roofer
@Roofer No, it does not. The Task<> cooked up by Task.FromResult is already in a completed state. With the result already available, its await will complete synchronously, as will the await in above it in Example1.Specie
One wonders, when it seems that the best practice advice is to pepper everything with ConfigureAwait(false), that that is not the default behavior?Photolithography
Would ConfigureAwait(false) on DoSomethingUnexpectedAsync() really solve the problem in this case? When we hit the second await, we are sure to be on a thread pool thread, so the SyncCtx is null. There is no difference in using ConfigureAwait(false) or not in this case, is it?Efren

© 2022 - 2024 — McMap. All rights reserved.