Skip to main content

Asynchronous Programming Using Async/Await in C#

By Kerem Ispirli
Programming
Share:

The async and await keywords were introduced in C# to make asynchronous programming on the .NET platform easier. These keywords have fundamentally changed how code is written in most of the C# ecosystem. Asynchronous programming has become mainstream, and modern frameworks such as ASP.NET Core are fully asynchronous.

Having such an impact on the C# ecosystem, asynchronous programming proves to be quite valuable. But what is asynchronous programming in the first place?

This article is going to introduce asynchronous programming, show usage of async and await keywords, talk about the deadlock pitfall and finish with some tips for refactoring blocking C# code with these keywords.

Let’s start with terminology.

Concurrent vs Parallel vs Asynchronous

What are the differences between these three terms? All of them are applications of multi-threading, their definitions overlap, and they are often used interchangeably. That’s why the terminology for implementations that leverage multi-threading can be confusing.

We’ll go through the nuances between these terms, so that we can come up with a clear definition for asynchronous programming.

Let’s assume a GUI application as an example.

Synchronous execution: doing things one after the other

The user clicks a button and waits for the application to finish handling the click event. Since only one thing can happen at a time, the UI stops responding until the event has been completely handled. In the same way, the application can’t do anything in the background while UI is available for user input.

Concurrent: doing multiple things at the same time

The user clicks a button, and the application triggers a separate thread in the background to execute the task needed to satisfy user’s request concurrently. The thread responsible for handling UI events becomes available again immediately after starting the background thread, keeping the UI responsive.

Parallel: doing multiple copies of something at the same time

The user instructs the application to process all the files in a folder. The application triggers a number of threads with the processing logic and distributes the files among these threads.

Asynchronous: not having to wait for one task to finish before starting another

The application starts a database query asynchronously. While the query is in progress, it also starts reading a file asynchronously. While both tasks are in progress, it does some calculation.
When all these tasks are finished, it uses the results of all these three operations to update the UI.

Asynchronous Programming

Based on the terminology above, we can define asynchronous programming simply as follows:

The execution thread should not wait for an I/O-bound or CPU-bound task to finish.

Examples of I/O-bound operations can be file system access, DB access, or an HTTP request. Examples of CPU-bound operations can be resizing an image, converting a document, or encrypting/decrypting data.

Benefits

Using asynchronous programming has several benefits:

  • avoiding thread pool starvation by “pausing” execution and releasing the thread back to thread pool during asynchronous activities
  • keeping the UI responsive
  • possible performance gains from concurrency

Asynchronous Programming Patterns

.NET provides three patterns for performing asynchronous operations.

Asynchronous programming model (APM): LEGACY

Also known as an IAsyncResult pattern, it’s implemented by using two methods: BeginOperationName and EndOperationName.

public class MyClass { 
    public IAsyncResult BeginRead(byte [] buffer, int offset, int count, AsyncCallback callback, object state) {...};
    public int EndRead(IAsyncResult asyncResult);
} 

From the Microsoft documentation:

After calling BeginOperationName, an application can continue executing instructions on the calling thread while the asynchronous operation takes place on a different thread. For each call to BeginOperationName, the application should also call EndOperationName to get the results of the operation.

Event-based asynchronous pattern (EAP): LEGACY

This pattern is implemented by writing an OperationNameAsync method and an OperationNameCompleted event:

public class MyClass { 
    public void ReadAsync(byte [] buffer, int offset, int count) {...};
    public event ReadCompletedEventHandler ReadCompleted;
} 

The asynchronous operation will be started with the async method, which will trigger the Completed event for making the result available when the async operation is completed. A class that uses EAP may also contain an OperationNameAsyncCancel method to cancel an ongoing asynchronous operation.

We have only an OperationNameAsync method that returns a Task or a generic Task<T> object:

public class MyClass { 
    public Task<int> ReadAsync(byte [] buffer, int offset, int count) {...};
} 

Task and Task<T> classes model asynchronous operations in TAP. It’s important to understand Task and Task<T> classes for understanding TAP, which is important for understanding and using async/await keywords, so let’s talk about these two classes in more detail.

Task and Task<T>

The Task and Task<T> classes are the core of asynchronous programming in .NET. They facilitate all kinds of interactions with the asynchronous operation they represent, such as:

  • adding continuation tasks
  • blocking the current thread to wait until the task is completed
  • signaling cancellation (via CancellationTokens)

After starting an asynchronous operation and getting a Task or Task<T> object, you can keep using the current execution thread to asynchronously execute other instructions that don’t need the result of the task, or interact with the task as needed.

Here’s some example code that uses tasks to visualize what it looks like in action:

using System;
using System.Threading.Tasks;

public class Example {
    public static void Main() {
       Task<DataType> getDataTask = Task.Factory.StartNew(() => { return GetData(); } );
       Task<ProcessedDataType> processDataTask = getDataTask.ContinueWith((data) => { return ProcessData(data);} );
       Task saveDataTask = processDataTask.ContinueWith((pData) => { SaveData(pData)} );
       Task<string> displayDataTask = processDataTask.ContinueWith((pData) => { return CreateDisplayString(pData); } );
       Console.WriteLine(displayDataTask.Result);
       saveDataTask.Wait();
    }
}

Let’s walk through the code:

  • We want to get some data. We use Task.Factory.StartNew() to create a task that immediately starts running. This task runs GetData() method asynchronously and, when finished, it assigns the data to its .Result property. We assign this task object to getDataTask variable.
  • We want to process the data that GetData() method will provide. Calling .ContinueWith() method, we asynchronously create another task and set it as a continuation to getDataTask. This second task will take the .Result of the first task as an input parameter (data) and call the ProcessData() method with it asynchronously. When finished, it will assign the processed data to its .Result property. We assign this task to the processDataTask variable. (It’s important to note that, at the moment, we don’t know whether getDataTask is finished or not, and we don’t care. We just know what we want to happen when it’s finished, and we write the code for that.)
  • We want to save the processed data. We use the same approach to create a third task that will call SaveData() asynchronously when data processing is finished, and set it as a continuation to processDataTask.
  • We also want to display the processed data. We don’t have to wait for the data to be saved before displaying it, so we create a fourth task that will create the display string from the processed data asynchronously when data processing is finished, and set it also as a continuation to processDataTask. (Now we have two tasks that are assigned as continuations to processDataTask. These tasks will start concurrently as soon as processDataTask is completed.)
  • We want to print the display string to the console. We call Console.WriteLine() with .Result property of the displayDataTask. The .Result property access is a blocking operation; our execution thread will block until displayDataTask is completed.
  • We want to make sure that the data is saved before leaving the Main() method and exiting the program. At this point, though, we do not know the state of saveDataTask. We call the .Wait() method to block our execution thread until saveDataTask completes.

Almost Good

As demonstrated above, TAP and Task/Task<T> classes are pretty powerful for applying asynchronous programming techniques. But there’s still room for improvement:

  • Boilerplate code needed for using tasks is quite verbose.
  • Assigning continuations and making granular decisions about which task should run means a lot of details should be handled by the programmer, increasing the complexity and making the code error-prone. (Verbosity, combined with increased complexity, means the code will be difficult to understand, thus difficult to maintain.)
  • Despite all this power, there’s no way to wait for a task to complete without blocking the execution thread.

These drawbacks can become significant challenges for teams to adopt TAP.

This is where the async and await keywords come into play.

async and await

These keywords have been introduced in response to these challenges of using the Task and Task<T> classes. They do not represent yet another way of asynchronous programming; they use Task and Task<T> classes under the hood, simplifying the application of TAP while maintaining the power Task classes provide to the programmer when needed.

Let’s take a look at each one.

async

The async keyword is added to the method signature to enable the usage of the await keyword in the method. It also instructs the compiler to create a state machine to handle asynchronicity, but that’s out of the scope of this article.

The return type of an async method is always Task or Task<T>. It’s checked by the compiler, so there’s not much room for making mistakes here.

await

The await keyword is used to asynchronously wait for a Task or Task<T> to complete. It pauses the execution of the current method until the asynchronous task that’s being awaited completes. The difference from calling .Result or .Wait() is that the await keyword sends the current thread back to the thread pool, instead of keeping it in a blocked state.

Under the hood, it:

  • creates a new Task or Task<T> object for the remainder of the async method
  • assigns this new task as a continuation to the awaited task,
  • assigns the context requirement for the continuation task

That last bit is also the part that causes deadlocks in some situations. We’re going to talk about it later, but first let’s see the async and await keywords in action.

What It Looks Like

Consider the following snippet from an ASP.NET application:

Let’s walk through the code:

  • Line 3: the execution thread enters the DoSomethingAndReturnSomeValAsync() method and calls DoSomething() method synchronously.
  • Line 4: the execution thread enters the DoSomethingElseAsync() method still synchronously, until the point where the Task<SomeType> is returned (not shown).
  • Upon returning to line 4 it encounters the await keyword, so it pauses the execution there and goes back to the thread pool.
  • Now the rest of DoSomethingElseAsync() is executing in the awaited Task<SomeType>.
  • Still line 4: As soon as the awaited task is finished, a thread is assigned from the thread pool to take over the execution of the rest of DoSomethingAndReturnSomeValAsync() method, continuing from the await keyword. This thread assigns the result of Task<SomeType> to someObj variable.
  • Line 5: The new thread constructs a ReturnType object, and returns it from the method.

Under the hood

When the execution thread encounters, the await keyword on line 4, it does the following:

  • It creates a Task<ReturnType> that contains the remainder of the DoSomethingAndReturnSomeValAsync() method.
  • It sets this new task as the continuation (as in Task.ContinueWith()) of the Task<SomeType> that was returned from DoSomethingElseAsync(), along with the required context.
  • Then it yields the execution and returns to the thread pool.

So the ReturnType object is actually returned from this Task<ReturnType> that is created by the await keyword. Hence the return type of Task<ReturnType> in the signature of our async method.

For further detail, see Task<TResult> and Task-based Asynchronous Pattern in MS docs.

How to Update Existing Code

The example below shows different ways of calling asynchronous methods from a synchronous method:

Since the DoWork() method is synchronous, the execution thread gets blocked three times:

  • at the .Result property one line 5
  • at the .Wait() method on line 7
  • at the .GetResult() method of the awaiter at line 8

Let’s walk through the code:

Line 5

The main execution thread enters the CallAnAPIAsync() method synchronously, until the point where a Task<ApiResult> is returned (not shown).

Upon returning to line 5 from the CallAnAPIAsync() method, it accesses the .Result property of the returned Task<ApiResult> object, which blocks the execution thread until the Task<ApiResult> is completed.

The main execution thread can’t do anything else at this moment; it just has to wait, doing nothing.

So at this moment our software is occupying two threads at once: the first one is the main execution thread which is blocked and just waiting, while the second one is the thread that executes the Task<ApiResult>.

After Task<ApiResult> completes and returns the ApiResult object, the main execution thread is unblocked. It assigns the ApiResult object to apiResult variable and moves on to line 6.

Line 6

This line is executed synchronously, so the execution thread completes the CreateFileName() method and assigns the return value to the fileName object, then moves on to line 7.

Line 7

This is almost the same as in line 5, except the lack of return value. Calling the .Wait() method blocks the execution thread until the Task from WriteToAFileAsync() method is completed.

Line 8

This is exactly the same as line 5: the ResultType object received from the blocking GetResult() method is returned from the DoWork() method.

Now, let’s rewrite the DoWork() method in an asynchronous way:

Here’s what we did:

Updating Method Signature

Line 1

  • Added the async keyword, enabling the await keyword for the method
  • Changed return type to Task\<ResultType>. (An async method should always return either a Task or a Task<T>.)
  • Appended the method name with “Async” per convention (excluding methods that aren’t explicitly called by our code such as event handlers and web controller methods).

Replacing Blocking Waits

Lines 5 and 7

Replaced blocking access to the .Result property with await.

Line 6

The opportunity of taking care of an independent activity concurrently gets into our radar once we engage in asynchronous programming.

I recommend figuring out the execution flow between line 5 and line 7 by yourself as an exercise.

Done? Here’s how it goes:

  • Instead of awaiting the Task<APIResult> immediately on line 5, the execution thread assigns the task to the apiResultTask variable. Then it continues executing line 6 concurrently, while a second thread is busy executing apiResultTask at the same time.

  • On line 7 the execution thread encounters the await keyword for the apiResultTask, so it pauses the execution and returns to the thread pool. As soon as the second thread completes the apiResultTask and returns an ApiResult object, the execution of DoWorkAsync() continues from line 7 by a thread from the thread pool. This thread will assign the ApiResult object to apiResult variable and move on to line 8.

Line 8

Replaced the blocking .Wait() call with await.

Line 9

Replaced blocking .GetAwaiter().GetResult() call with await.

Notice that the StartAsyncOperation() method doesn’t have to be async itself; it returns a Task<T>, which is awaitable.

Exception Handling

Line 10

Replaced AggregateException with RealException. If an error occurs, all blocking waits throw an AggregateException, which wraps whatever exception(s) are thrown from the Task. The await keyword, on the other hand, throws the actual exception.

If a Task consists of multiple Tasks, then it becomes possible to have multiple exceptions aggregated by the main Task.

If you’re awaiting such a Task, then the await keyword will throw only the first exception! After catching the first exception, you can use the Task.Exception property of the main Task to access the AggregateException.

Now that the DoWorkAsync() method is asynchronous, every time the execution thread encounters the await keyword it will pause the execution of DoWorkAsync() and go back to thread pool, instead of being blocked until the async operation is completed.

Remarks

These remarks apply only to .NET Framework and ASP.NET applications. .NET Core and ASP.NET Core don’t have the SynchronizationContext that causes the issues explained below. See here for details.

On the other hand, if you’re writing a library that may be used by a .NET Framework application, then you should always consider these.

Avoiding Deadlocks

Executing async operations synchronously by blocking the execution thread brings the risk of creating a deadlock.

Can you find the deadlock below?

At line 5, the execution thread is blocked, waiting for the .Result of the Task<SomeType> of DoSomethingAsync() method.

At line 10, the DoSomethingAsync() method receives a Task<> from the GetDataAsync() method and awaits it.

The await keyword:

  • creates a Task<SomeType> that contains rest of this method
  • assigns this Task<SomeType> as the continuation of the awaited Task<>
  • sets this Task<SomeType> to run within the context of the execution thread
  • returns this Task<SomeType> object to line 5

So the Task<> from GetDataAsync() completes normally, but accessing the .Result property of Task<SomeType> from DoSomethingAsync() on line 5 still blocks the execution thread until Task<SomeType> is completed. Meanwhile, Task<SomeType> is waiting for the execution thread to be available because it needs the context of the execution thread. As a result, we have a deadlock.

The workflow with the deadlock is visualized in the sequence diagram below.

Deadlock Sequence

There are two solutions:

  1. Do not block on async code. Update the whole call stack to be async.

    We can achieve this by making HandleRESTApiCall() method async and replacing .Result access on line 5 with an await.

    Removing the blocking .Result call solves the deadlock.

    public class MyApiController : ApiController
    {
      // Top-level method
      public async ActionResult HandleRESTApiCall(){
        SomeType someObj = await DoSomethingAsync();
        return OkResult(someObj);
      }
    ...
    }
    
  2. Use .ConfigureAwait(false) on line 10

    Details of this solution is discussed in ConfigureAwait() topic below.

    var someData = await GetDataAsync().ConfigureAwait(false);
    

It’s best to use both solutions, since it will bring the best performance.

Sometimes solution 1 may cause too many code changes when applied as a boy scout. In that case, I recommend first applying solution 2 to avoid the deadlock, and then creating a separate change as soon as possible to make the whole call stack async.

Actually, there’s a third way. On line 5, we can wrap the DoSomethingAsync() call in another Task:

SomeType someObj = Task.Run(() => DoSomethingAsync()).Result;

But this is more of a Band-Aid fix than a real solution. Use this technique only when you can’t change the DoSomethingAsync() method, and make sure to follow with a separate change to make the whole call stack async and remove the wrapping task.

ConfigureAwait(bool continueOnCapturedContext)

Every execution thread comes with a context. In GUI applications, the context contains UI elements such as TextBoxes or Buttons. In ASP.NET applications, the context contains HttpContext.Current and enables building an ASP.NET response, including return statements in controller actions.

Once the execution of an async method is paused at an await keyword, the continuation is set to run in the context of the calling thread by default. This can cause deadlocks (see the “Avoiding Deadlocks” section above), is rarely necessary and has a slight negative performance impact.

Instead, we can configure the continuation to run context-free. To do this, we call the .ConfigureAwait(false) method. See the example below:

public class MyApiController : ApiController
{
  // Top-level method
  public async Task<ActionResult> HandleRESTApiCall(){
    SomeType someObj = await DoSomethingAsync(); // no .ConfigureAwait(false) because...
    // ...we need the context here in the continuation!
    return OkResult(someObj);
  }

  private async Task<SomeType> DoSomethingAsync(){
    var someData = await GetDataAsync().ConfigureAwait(false);   // <==   Here it is!
    // We don't need the context here
    return new SomeType(someData);
  }
}

You can find more details in Microsoft documentation.

Summary

  • Asynchronous programming can be defined as not making the execution thread wait for an I/O-bound or CPU-bound task to finish.
  • Asynchronous programming is necessary for a responsive GUI. It increases the throughput by using thread pool efficiently and enables increasing performance via concurrency.
  • There are various patterns for asynchronous programming in .NET. The recommended pattern is Task-based Asynchronous Pattern (TAP).
  • The async and await keywords make using TAP easier and enable non-blocking waits.
  • Combining blocking waits such as .Wait() or .Result with async/await in .NET Framework can result in deadlocks. This is highly relevant when refactoring legacy code.
  • Such deadlocks can be solved by using .ConfigureAwait(false) or removing the blocking waits and making the whole call stack async. The recommendation is to apply these solutions together for best performance.

Further Reading

More information

Thanks to Rick de Korte, Erik van de Ven and Ben Dickson for reviewing this article.

Kerem is a software engineer who is passionate about removing waste from software development at every step of the lifecycle, from designing for testability to efficient CI/CD and DevOps.

Integromat Tower Ad