How to use async and await in .Net Core

Follow these best practices when using asynchronous programming in .Net Core to build more performant and scalable applications

Asynchronous programming allows you to write programs that don’t block on each statement or instruction, meaning the computer can move on to other tasks before waiting for previous tasks to finish. As a result, asynchronous programming enables you to build applications that are more scalable and responsive.

The async and await keywords allow us to write asynchronous code. These have been optimized in .Net Core for ease of use and performance. This article discusses a few points that you should be aware of when working with asynchronous programming in .Net Core applications.

The async and await keywords

An asynchronous method is one that is marked with the async keyword in the method signature. It can contain one or more await statements. It should be noted that await is a unary operator — the operand to await is the name of the method that needs to be awaited. The point at which the await keyword is encountered is known as the suspension point. The following code snippet illustrates how the async and await keywords are used.

public async Task GetDataAsync()
{
  using (SqlConnection connection = new
  SqlConnection(connectionString)
  {
     await connection.OpenAsync();
     //Other code   
  }
}

Asynchronous continuations

When you use the await keyword in an asynchronous method, the method is split up inside a state machine. The await keyword captures the current synchronization context. Then, as soon as the task that has been awaited has completed its execution, the state machine resumes execution of the code in the caller method. This is also known as continuation. Note that the continuation must wait for other continuations that might have been queued already. Most importantly, only one continuation can execute at a given point in time in ASP.Net.

When the awaited task is ready to execute, a thread from the thread pool enters the request context and resumes the execution of the asynchronous handler. This re-entering of the request context is a costly operation because it involves several tasks that include setting the HttpContext.Current and the identity and culture of the current thread.

SynchronizationContext 

The ExecutionContext contains relevant metadata about the current environment or context in which the program is executing. The SynchronizationContext, which is available in the System.Threading namespace in the .Net Framework, represents the location in which the code is executed.

However, note that we no longer have the SynchronizationContext class in ASP.Net Core. This is because the async and await mechanism in ASP.Net Core has been optimized and simplified: AspNetSynchronizationContext has been removed from ASP.Net Core for ease of use and performance. When an asynchronous handler resumes execution in ASP.Net Core, a thread from the thread pool executes the continuation. The context queue is avoided.

ConfigureAwait(false)

An asynchronous handler resumes execution on the original thread—the thread that started it. If any other operation has taken control of this thread, the handler will have to wait until the operation relinquishes control.

However, when you use ConfigureAwait(false) in your code, the continuation may resume on any thread — it doesn’t need to wait for the original thread. In other words, the ConfigureAwait(false) method is used to ensure that the task continuation doesn’t resume on the captured context, i.e., the caller’s context. It is a good practice to use this method in legacy ASP.Net to avoid deadlocks.

Note that when the continuation resumes on another thread, the thread synchronization context is lost along with any culture or language information (i.e. the writing system, calendar, date and number formatting, etc.). You can store this information before you make a call to await and then re-apply the settings on the new thread. 

Note that in ASP.Net Core a call to ConfigureAwait(false) is redundant and does nothing. Because there is no longer a synchronization context in ASP.Net Core, you no longer need to include ConfigureAwait(false) in ASP.Net Core, but including it does no harm. You should use ConfigureAwait(false) in your library code since the library can be reused in other applications. 

Avoid implicit parallelism in .Net Core

When working in ASP.Net Core you should be aware of implicit parallelism. Let me explain what implicit parallelism is and why we should care. In legacy ASP.Net you have a synchronization context and asynchronous continuations can execute on any thread. However, inside the request context only one thread can execute the code at a time. 

By contrast, ASP.Net Core lacks a synchronization context and asynchronous continuations can execute on any thread. Most importantly, continuations can even execute on multiple threads in parallel. And that’s where the problem lies. Let’s understand this with a code example.

The following piece of code will run just fine in ASP.Net.

private HttpClient _httpClient = new HttpClient();
public Task ExecuteParallelJobs(List<string> urls)
{
    List<string> result = new List<string>();
    var jobs = new Task[5];
    for(int i=0; i < 5; i++)
    {
      job[i] = GetDataAsync(result, urls[i].ToString());
    }
   await Task.WhenAll(jobs);
   return result;
}
private async Task GetDataAsync(List<string> result, string url)
{
   var data = await _client.GetStringAsync(url);
   result.Add(data);
}

The statement result.Add(data) in the GetDataAsync method will be executed in the request context. Hence it will be executed by only one thread at a time in ASP.Net. However, the same statement, result.Add(data), will be executed by multiple threads in ASP.Net Core, without protecting the shared collection instance named result.

To solve this problem, you can use concurrent collections like ConcurrentBag<T> or ConcurrentStack<T> or use the lock keyword as shown in the code snippet below.

private async Task GetDataAsync(List<string> result, string url)
{
   var data = await _client.GetStringAsync(url);
   lock(result)
   {
     result.Add(data);
   } 
}

Asynchronous programming allows us to perform resource-intensive operations without blocking on the main or the executing thread of the application. When used the right way, asynchronous programming can help boost your application’s scalability considerably. However, you should adhere to the recommended practices and guidelines—and mind the differences between ASP.Net Core and legacy ASP.Net. 

Copyright © 2019 IDG Communications, Inc.