- Item 27: Use Async Methods for Async Work
- Item 28: Never Write async void Methods
- Item 29: Avoid Composing Synchronous and Asynchronous Methods
- Item 30: Use Async Methods to Avoid Thread Allocations and Context Switches
- Item 31: Avoid Marshalling Context Unnecessarily
- Item 32: Compose Asynchronous Work Using Task Objects
- Item 33: Consider Implementing the Task Cancellation Protocol
- Item 34: Cache Generalized Async Return Types
Item 31: Avoid Marshalling Context Unnecessarily
We refer to code that can run in any synchronization context as “context-free code.” In contrast, code that must run in a specific context is “context-aware code.” Most of the code you write is context-free code. Context-aware code includes code in a GUI application that interacts with UI controls, and code in a Web application that interacts with HTTPContext or related classes. When context-aware code executes after an awaited task has completed, it must be executed on the correct context (see Item 27). However, all other code can execute in the default context.
With so few locations where code is context aware, you might wonder why the default behavior is to execute continuations on the captured context. In fact, the consequences of switching contexts unnecessarily are much less onerous than the consequences of not switching contexts when it is necessary. If you execute context-free code on the captured context, nothing is likely to go drastically wrong. Conversely, if you execute context-aware code on the wrong context, your application will probably crash. For this reason, the default behavior is to execute the continuation on the captured context, whether it is necessary or not.
While resuming on the captured context may not cause drastic problems, it will still cause problems that may become compounded over time. By running continuations on the captured context, you don’t ever take advantage of any opportunities to offload some continuations to other threads. In GUI applications, this can make the UI unresponsive. In Web applications, it can limit how many requests per minute the application can manage. Over time, performance will degrade. In the case of GUI applications, you increase the chance of deadlock (see Item 39). In Web applications, you don’t fully utilize the thread pool.
The way around these undesirable outcomes is to use ConfigureAwait() to indicate that continuations do not need to run on the captured context. In library code, where the continuation is context-free code, you would use it like this:
public static async Task<XElement> ReadPacket(string Url)
{
var result = await DownloadAsync(Url)
.ConfigureAwait(continueOnCapturedContext: false);
return XElement.Parse(result);
}
In simple cases, it’s easy. You add ConfigureAwait() and your continuation will run on the default context. Consider this method:
public static async Task<Config> ReadConfig(string Url)
{
var result = await DownloadAsync(Url)
.ConfigureAwait(continueOnCapturedContext: false);
var items = XElement.Parse(result);
var userConfig = from node in items.Descendants()
where node.Name == "Config"
select node.Value;
var configUrl = userConfig.SingleOrDefault();
if (configUrl != null)
{
result = await DownloadAsync(configUrl)
.ConfigureAwait(continueOnCapturedContext: false);
var config = await ParseConfig(result)
.ConfigureAwait(continueOnCapturedContext: false);
return config;
}
else
return new Config();
}
You might think that once the first await expression is reached, the continuation runs on the default context, so ConfigureAwait() is not needed on any of the subsequent async calls. That assumption may be wrong. What if the first task completes synchronously? Recall from Item 27 that this would mean the work continues synchronously on the captured context. Execution would then reach the next await expression while still within the original context. The subsequent calls won’t continue on the default context, so all the work continues on the captured context.
For this reason, whenever you call a Task-returning async method and the continuation is context-free code, you should use ConfigureAwait(false) to continue on the default context. Your goal is to isolate the context-aware code to code that must manipulate the UI. To see how this works, consider the following method:
private async void OnCommand(object sender, RoutedEventArgs e)
{
var viewModel = (DataContext as SampleViewModel);
try
{
var userInput = viewModel.webSite;
var result = await DownloadAsync(userInput);
var items = XElement.Parse(result);
var userConfig = from node in items.Descendants()
where node.Name == "Config"
select node.Value;
var configUrl = userConfig.SingleOrDefault();
if (configUrl != null)
{
result = await DownloadAsync(configUrl);
var config = await ParseConfig(result);
await viewModel.Update(config);
}
else
await viewModel.Update(new Config());
}
catch (Exception ex) when (logMessage(viewModel, ex))
{
}
}
This method is structured so that it is difficult to isolate the context-aware code. Several asynchronous methods are called from this method, most of which are context free. However, the code toward the end of the method updates UI controls and is context aware. You should treat all awaitable code as context free, unless it will update user interface controls. Only code that updates the user interface is context aware.
As written, the example method must run all its continuations on the captured context. Once any continuations start on the default context, there’s no easy way back. The first step to remedy this problem is to restructure the code so that all the context-free code is moved to a new method. Then, once that restructuring is done, you can add the ConfigureAwait(false) calls on each method to run the asynchronous continuations on the default context:
private async void OnCommand(object sender, RoutedEventArgs e)
{
var viewModel = (DataContext as SampleViewModel);
try
{
Config config = await ReadConfigAsync(viewModel);
await viewModel.Update(config);
}
catch (Exception ex) when (logMessage(viewModel, ex))
{
}
}
private async Task<Config> ReadConfigAsync(SampleViewModel
viewModel)
{
var userInput = viewModel.webSite;
var result = await DownloadAsync(userInput)
.ConfigureAwait(continueOnCapturedContext: false);
var items = XElement.Parse(result);
var userConfig = from node in items.Descendants()
where node.Name == "Config"
select node.Value;
var configUrl = userConfig.SingleOrDefault();
var config = default(Config);
if (configUrl != null)
{
result = await DownloadAsync(configUrl)
.ConfigureAwait(continueOnCapturedContext: false);
config = await ParseConfig(result)
.ConfigureAwait(continueOnCapturedContext: false);
}
else
config = new Config();
return config;
}
Matters might have been simpler if the default continued on the default context. This approach, however, would have meant that getting it wrong would lead to crashes. With the current strategy, your application will run if all continuations use the captured context, but it will run less efficiently. Your users deserve better. Structure your code to isolate the code that must run on the captured context. Use ConfigureAwait(false) to continue on the default context whenever possible.