Understanding Async and Await in .NET

Async and await keywords in .NET are widely misunderstood, often leading to misuse and confusion. A common misconception is that async and...

Written by Bala · 5 min read >

Async and await keywords in .NET are widely misunderstood, often leading to misuse and confusion. A common misconception is that async and await facilitate parallel or concurrent execution. This blog aims to clarify their purpose and demonstrate their correct usage with examples, focusing on asynchronous programming in C# 12 and .NET 8.

To illustrate the correct use of async and await, consider a simple application that simulates search for a word or a phrase in more than one location i.e. external service, different types of databases or even in the local file system.

Here are two versions of this application: one synchronous and one asynchronous. In the synchronous version, a search keyword or a phrase is searched in multiple locations sequentially. The program also prints the time it took to execute the following in the end.

using System;
using System.Diagnostics;
using System.Threading;
using System.Collections.Generic;

public class MultiSourceSearcher
{
    public static void Main(string[] args)
    {
        string searchTerm = "example";
        InitializeConnections();
        
        var stopwatch = Stopwatch.StartNew();

        bool found = SearchInRedis(searchTerm) ||
                     SearchInMySQL(searchTerm) ||
                     SearchInPostgres(searchTerm) ||
                     SearchInMongoDB(searchTerm) ||
                     SearchInExternalAPI(searchTerm) ||
                     SearchInAzureBlobStorage(searchTerm) ||
                     SearchInInMemoryDatabase(searchTerm) ||
                     SearchInLocalFileSystem(searchTerm);

        stopwatch.Stop();
        Console.WriteLine(found ? "Search term found!" : "Search term not found.");
        Console.WriteLine($"Time taken: {stopwatch.ElapsedMilliseconds} ms");
    }

    private static void InitializeConnections()
    {
        Console.WriteLine("Initializing Redis connection...");
        Console.WriteLine("Initializing MySQL connection...");
        Console.WriteLine("Initializing Postgres connection...");
        Console.WriteLine("Initializing MongoDB connection...");
        Console.WriteLine("Initializing Azure Blob Storage connection...");
        Console.WriteLine("Initializing In-Memory Database...");
    }

    private static bool SearchInRedis(string searchTerm)
    {
        Console.WriteLine($"Searching in Redis for '{searchTerm}'...");
        Thread.Sleep(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static bool SearchInMySQL(string searchTerm)
    {
        Console.WriteLine($"Searching in MySQL for '{searchTerm}'...");
        Thread.Sleep(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static bool SearchInPostgres(string searchTerm)
    {
        Console.WriteLine($"Searching in Postgres for '{searchTerm}'...");
        Thread.Sleep(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static bool SearchInMongoDB(string searchTerm)
    {
        Console.WriteLine($"Searching in MongoDB for '{searchTerm}'...");
        Thread.Sleep(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static bool SearchInExternalAPI(string searchTerm)
    {
        Console.WriteLine($"Searching in External API for '{searchTerm}'...");
        Thread.Sleep(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static bool SearchInAzureBlobStorage(string searchTerm)
    {
        Console.WriteLine($"Searching in Azure Blob Storage for '{searchTerm}'...");
        Thread.Sleep(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static bool SearchInInMemoryDatabase(string searchTerm)
    {
        Console.WriteLine($"Searching in In-Memory Database for '{searchTerm}'...");
        Thread.Sleep(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static bool SearchInLocalFileSystem(string searchTerm)
    {
        Console.WriteLine($"Searching in Local File System for '{searchTerm}'...");
        Thread.Sleep(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }
}

In the asynchronous version, tasks are defined with the async keyword and awaited using the await keyword, but the order of execution remains the same. Similar to the synchronous example this too outputs the time it took to complete the entire run.

using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Collections.Generic;

public class MultiSourceSearcher
{
    public static async Task Main(string[] args)
    {
        string searchTerm = "example";
        InitializeConnections();
        
        var stopwatch = Stopwatch.StartNew();

        bool found = await SearchInRedisAsync(searchTerm) ||
                     await SearchInMySQLAsync(searchTerm) ||
                     await SearchInPostgresAsync(searchTerm) ||
                     await SearchInMongoDBAsync(searchTerm) ||
                     await SearchInExternalAPIAsync(searchTerm) ||
                     await SearchInAzureBlobStorageAsync(searchTerm) ||
                     await SearchInInMemoryDatabaseAsync(searchTerm) ||
                     await SearchInLocalFileSystemAsync(searchTerm);

        stopwatch.Stop();
        Console.WriteLine(found ? "Search term found!" : "Search term not found.");
        Console.WriteLine($"Time taken: {stopwatch.ElapsedMilliseconds} ms");
    }

    private static void InitializeConnections()
    {
        Console.WriteLine("Initializing Redis connection...");
        Console.WriteLine("Initializing MySQL connection...");
        Console.WriteLine("Initializing Postgres connection...");
        Console.WriteLine("Initializing MongoDB connection...");
        Console.WriteLine("Initializing Azure Blob Storage connection...");
        Console.WriteLine("Initializing In-Memory Database...");
    }

    private static async Task<bool> SearchInRedisAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in Redis for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static async Task<bool> SearchInMySQLAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in MySQL for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static async Task<bool> SearchInPostgresAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in Postgres for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static async Task<bool> SearchInMongoDBAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in MongoDB for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static async Task<bool> SearchInExternalAPIAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in External API for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static async Task<bool> SearchInAzureBlobStorageAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in Azure Blob Storage for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static async Task<bool> SearchInInMemoryDatabaseAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in In-Memory Database for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static async Task<bool> SearchInLocalFileSystemAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in Local File System for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }
}

From the duration output of both the programs, it's evident now that await and async do not inherently make the process faster by running the operations in parallel.

Async and await are designed to prevent blocking of threads. In a synchronous application, long-running tasks block the thread, leading to unresponsive user interfaces or inefficient server handling.

In a desktop application, blocking the UI thread with a long-running task causes the UI to freeze. In web applications, blocking threads can lead to server inefficiency under load.

Parallel Programming with .NET

Parallel programming in .NET involves running tasks concurrently on multiple threads. This can be achieved using the Task.WhenAll method, which runs multiple tasks in parallel and waits for all to complete.

using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

public class MultiSourceSearcher
{
    public static async Task Main(string[] args)
    {
        string searchTerm = "example";
        InitializeConnections();
        
        var stopwatch = Stopwatch.StartNew();

        var tasks = new List<Task<bool>>
        {
            SearchInRedisAsync(searchTerm),
            SearchInMySQLAsync(searchTerm),
            SearchInPostgresAsync(searchTerm),
            SearchInMongoDBAsync(searchTerm),
            SearchInExternalAPIAsync(searchTerm),
            SearchInAzureBlobStorageAsync(searchTerm),
            SearchInInMemoryDatabaseAsync(searchTerm),
            SearchInLocalFileSystemAsync(searchTerm)
        };

        bool[] results = await Task.WhenAll(tasks);
        bool found = results.Any(result => result);

        stopwatch.Stop();
        Console.WriteLine(found ? "Search term found!" : "Search term not found.");
        Console.WriteLine($"Time taken: {stopwatch.ElapsedMilliseconds} ms");
    }

    private static void InitializeConnections()
    {
        Console.WriteLine("Initializing Redis connection...");
        Console.WriteLine("Initializing MySQL connection...");
        Console.WriteLine("Initializing Postgres connection...");
        Console.WriteLine("Initializing MongoDB connection...");
        Console.WriteLine("Initializing Azure Blob Storage connection...");
        Console.WriteLine("Initializing In-Memory Database...");
    }

    private static async Task<bool> SearchInRedisAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in Redis for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static async Task<bool> SearchInMySQLAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in MySQL for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static async Task<bool> SearchInPostgresAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in Postgres for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static async Task<bool> SearchInMongoDBAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in MongoDB for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static async Task<bool> SearchInExternalAPIAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in External API for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static async Task<bool> SearchInAzureBlobStorageAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in Azure Blob Storage for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static async Task<bool> SearchInInMemoryDatabaseAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in In-Memory Database for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }

    private static async Task<bool> SearchInLocalFileSystemAsync(string searchTerm)
    {
        Console.WriteLine($"Searching in Local File System for '{searchTerm}'...");
        await Task.Delay(new Random().Next(3000, 10000)); // Random delay between 3 to 10 seconds
        return false; // Simulated result
    }
}

This approach reduced the total time to almost half by running tasks in parallel, effectively utilizing multiple threads.

Conclusion

Async and await are essential for writing non-blocking code in .NET, improving application responsiveness and efficiency. However, they do not inherently provide parallel execution. For parallel processing, tools like Task.WhenAll should be used. Misunderstanding these concepts can lead to inefficient and incorrect code.