best practices of async await and asynchronous programming

Asynchronous programming has been widely used in C# almost in every kind of .Net application and async and await keywords have been the core of the same which hides most of the complexity to implement asynchronous programming in C# language and makes it easy for developers to implement asynchronous programming.

Let’s see what the compiler does behind the scenes when we use the async/await keyword in our method, the compiler turns the method into a generated class that implements the IAsyncStateMachine interface.

Using .ConfigureAwait(false)


As we know we have Synchronization context in .Net Framework where the context is retained after the async calls are completed and this is useful in GUI applications where we need to continue with the original thread which initiated the call but in libraries, this is not required and changing the context doesn’t hamper the application so using Task.ConfigureAwait(false) boosts the application performance because we don’t have to switch the context and the application can continue using its current context.

async Task SomeMethodAsync()

{

  // Code here runs in the original context.

  await Task.Delay(1000);

 


// Code here runs without the original context (in this case, on the thread pool).

  await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);

}

Avoiding .Result and .Wait


Asynchronous programming provides performance benefits because it doesn’t block the current thread and while the I/O-bound or CPU-bound work continues in the background the main thread responds to user interaction in GUI application but if we use .Result or .Wait with await in C# then it blocks the main thread until the I/O-bound or CPU-bound completes and it may result in deadlock as well. As part of best practices, we shouldn’t convert a small part of our application to asynchronous and wrap them into synchronous API which creates a hard-to-track deadlock problem therefore we should allow async code to grow naturally to our entire code base and embrace async all the way.

Another disadvantage of .Wait and .Result is that it throws Aggregate Exception instead of normal exception which complicates error handling.


Using .GetAwaiter().GetResult()


In some rare scenarios if we want to block an asynchronous task synchronous then instead of using .Wait or .Result we should use .GetAwaiter().GetResult() so that in case of error, the application throws normal exception instead of the Aggregate Exception which is thrown by .Wait or .Result .

Please note .GetAwaiter().GetResult() is also synchronously blocking the async Task and is subject to deadlock problems just as .Wait or .Result but gives better error handling so I will go for .GetAwaiter().GetResult() in case of handful situation.

Avoiding Async void


Any Async method can return Task or Task<T> or void but returning void from an async method doesn’t allow to catch the exception naturally and can be only caught in AppDomain.UnhandledException or similar catch-all event for GUI/asp.net applications. Async void methods will only notify the SynchronizationContext when they start or finish and maintaining the same is difficult for regular applications.


Generally, when the exception is thrown out of an async Task or async Task<T> method, the exception is captured and placed on the Task object but with an async void there is no Task object so the exception is directly raised to the SynchronizationContext that was active when the async void method started.


private async void SomeMethodExceptionAsync()

{

  throw new InvalidOperationException();

}

public void AsyncVoidExceptions_CannotBeCaughtByCatch()

{

  try

  {

    SomeMethodExceptionAsync();

  }

  catch (Exception ex)

  {

    // The exception is never caught here!

    throw;

  }

}

Async all the way


Async codes work best when we use async all the way to the entry point e.g. event handlers. We should allow async code to grow naturally in our code base so that we don’t have to use .Wait or .Result which blocks the main thread and results in a deadlock.

Following code will cause the common deadlock problem when blocking async code

public static class DeadlockDemo

{

  private static async Task DelayAsync()

  {

    await Task.Delay(1000);

  }

  // This method causes a deadlock when called in a GUI or ASP.NET context.

  public static void TestMethod()

  {

    // Start the delay.

    var delayTask = DelayAsync();

    // Wait for the delay to complete.

    delayTask.Wait();

  }

}


The root cause of this deadlock is the way await handles contexts. By default, when an incomplete Task is awaited, the current “context” is captured and used to resume the method when the Task completes. This “context” is the current SynchronizationContext unless it’s null, in which case it’s the current TaskScheduler. GUI and ASP.NET applications have a SynchronizationContext that permits only one chunk of code to run at a time. When the await completes, it attempts to execute the remainder of the async method within the captured context. But that context already has a thread in it, which is (synchronously) waiting for the async method to complete. They’re each waiting for the other, causing a deadlock.

To Summarize, we should avoid mixing async and blocking code which can cause deadlock, more complex error handling, and unexpected blocking of context threads.


Compulsory await in all async method


We shouldn’t have an async method that doesn’t have the await in it because if you don’t have the await in the async method the compiler will not wait for the Task to complete or an exception thrown will not be caught in the async method. Thus, if you want to use async without await then you don’t bother with the result and just want to fire and forget.


Cancelling the Task


Cancelling an ongoing Task may be useful in some scenarios like a user clicking the submit button twice or similar and .Net Framework facilitates canceling the execution of the Task using CancellationToken class where we can pass the CancellationToken to the entire flow so that if cancellation is requested then it can cancel the execution of the Tasks.

In the most common case, cancellation follows this flow:

  1. The caller creates a CancellationTokenSource object.

  2. The caller calls a cancelable async API and passes the CancellationToken from the CancellationTokenSource (CancellationTokenSource.Token).

  3. The caller requests cancellation using the CancellationTokenSource object (CancellationTokenSource.Cancel()).

  4. The task acknowledges the cancellation and cancels itself, typically using the CancellationToken.ThrowIfCancellationRequested method.

Note that for this mechanism to work, you will need to write code to check for the cancellation being requested at regular intervals (i.e: on every iteration of your code, or at a natural stopping point in the logic). Ideally, once the cancellation has been requested, an async Task should cancel as quickly as possible.

You should consider using cancellation for all methods that may take a long time to complete.


Conclusion


I hope the above best practices will help you to write asynchronous programming efficiently and also help you to avoid common problems like deadlock and improve your application performance and responsiveness.

Share This Post

Linkedin
Fb Share
Twitter Share
Reddit Share

Support Me

Buy Me A Coffee