DEV Community

Sandeep Borhade
Sandeep Borhade

Posted on

Understanding Async and Await in C#

Lot of time I have been asked the question around Async and Await during interviews.
Here is I am trying to consolidate my understanding around these keywords with some snippets.

What is Asynchronous Programming?:
In Asynchronous programming methods are run without blocking the main thread.

**Async **Keyword: Marks a method as asynchronous, allowing the use of await.
**Await **Keyword: Suspends execution until the awaited task is complete.
Task and Task: Core types representing asynchronous operations.

Why Use await in Methods?
The await keyword is used to pause execution of the calling method until the awaited asynchronous operation completes. This allows other tasks (like UI updates or other background operations) to run without blocking the thread.

When Not to Use await

In some cases, you might not need await:

Fire-and-Forget: When the result of a task isn’t needed, you can call it without await.

LogDataAsync("Message"); // Fire-and-forget (but use cautiously)
Enter fullscreen mode Exit fullscreen mode

Aggregating Multiple Tasks: You can combine tasks using Task.WhenAll without awaiting each individually.

var tasks = new[] { Task1(), Task2() };
await Task.WhenAll(tasks);

Enter fullscreen mode Exit fullscreen mode

Returning Tasks: When a method simply returns a task to its caller, you don't need await unless you want to handle exceptions or perform additional work.

static Task<string> FetchDataAsync() => Task.FromResult("Data");
Enter fullscreen mode Exit fullscreen mode

To make the operations parallel or concurrent, you need to execute multiple tasks simultaneously without waiting for one task to complete before starting the next. This can improve performance, especially for I/O-bound operations like database calls, API requests, or file logging.

In the banking example, here’s how parallelism or concurrency can be applied:


Understanding Parallelism vs Concurrency

  • Parallelism: Tasks run at the same time on multiple threads, often leveraging multiple CPU cores. It’s best for CPU-bound tasks.
  • Concurrency: Tasks appear to run simultaneously by interleaving execution, often best for I/O-bound tasks (e.g., waiting for a database or API response).

In financial example:

  • Concurrent tasks: Fetching account balance, calling external APIs, and logging transactions.
  • Parallel tasks: Performing multiple transfers or operations simultaneously.

Banking Example for Parallelism

Scenario

We'll make the following tasks run concurrently:

  1. Checking the account balance.
  2. Making the API call for fund transfer.
  3. Logging the transaction.

These tasks are independent and can run simultaneously.

Updated Code

using System;
using System.IO;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

class BankingService
{
    private static readonly HttpClient _httpClient = new HttpClient();

    public async Task MainAsync()
    {
        Console.WriteLine("Transaction started...");

        // Step 1: Run tasks concurrently
        var checkBalanceTask = CheckAccountBalanceFromDbAsync("123456");
        var transferTask = TransferFundsToExternalBankAsync("123456", "654321", 500);
        var logTask = LogTransactionAsync("123456", "654321", 500);

        // Step 2: Await all tasks to complete
        await Task.WhenAll(checkBalanceTask, transferTask, logTask);

        Console.WriteLine("Transaction completed successfully.");
    }

    // Simulate checking account balance (database operation)
    private async Task<decimal> CheckAccountBalanceFromDbAsync(string accountId)
    {
        Console.WriteLine("Querying account balance from database...");
        await Task.Delay(500); // Simulate delay for database query
        Console.WriteLine("Account balance fetched.");
        return 1000.00m; // Assume a balance of $1000
    }

    // Simulate transferring funds to another bank using an external API
    private async Task TransferFundsToExternalBankAsync(string fromAccountId, string toAccountId, decimal amount)
    {
        Console.WriteLine($"Transferring ${amount} from {fromAccountId} to {toAccountId}...");

        var payload = new
        {
            fromAccountId,
            toAccountId,
            amount
        };

        var jsonPayload = JsonSerializer.Serialize(payload);
        var content = new StringContent(jsonPayload, System.Text.Encoding.UTF8, "application/json");

        var response = await _httpClient.PostAsync("https://mock-bank-api.com/transfer", content);

        if (response.IsSuccessStatusCode)
        {
            Console.WriteLine("Funds transferred successfully.");
        }
        else
        {
            Console.WriteLine("Transfer failed.");
        }
    }

    // Simulate logging the transaction (write to a file)
    private async Task LogTransactionAsync(string fromAccountId, string toAccountId, decimal amount)
    {
        string logMessage = $"Transaction: ${amount} from {fromAccountId} to {toAccountId} at {DateTime.Now}\n";
        Console.WriteLine("Logging transaction...");
        await File.AppendAllTextAsync("transaction_log.txt", logMessage);
        Console.WriteLine("Transaction logged.");
    }
}

// Entry point
class Program
{
    static async Task Main(string[] args)
    {
        var service = new BankingService();
        await service.MainAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

How This Works

  1. Concurrent Execution:

    • Task.WhenAll allows multiple tasks to run concurrently.
    • The following three tasks start simultaneously:
      • Checking the account balance.
      • Transferring funds.
      • Logging the transaction.
  2. Efficiency:

    • Each task is asynchronous and doesn’t block the thread while waiting for I/O operations (e.g., database query, HTTP request, or file writing).
    • The tasks do not depend on each other and can run independently.
  3. Result Handling:

    • If you need the result from CheckAccountBalanceFromDbAsync, you can await it separately while other tasks still execute:
     decimal balance = await checkBalanceTask;
    

Key Concepts:

Task.WhenAll

  • Waits for all tasks to complete.
  • If any task fails, it throws an AggregateException containing details of all failures.
  • Example:
  await Task.WhenAll(task1, task2, task3);
Enter fullscreen mode Exit fullscreen mode

Task.WhenAny

  • Waits for the first task to complete (useful for racing or fallback scenarios).
  • Example:
  var firstCompletedTask = await Task.WhenAny(task1, task2, task3);
Enter fullscreen mode Exit fullscreen mode

Advantages of Parallel/Concurrent Execution

  1. Improved Performance:
    • Independent tasks run simultaneously, reducing the overall time.
  2. Non-Blocking Execution:
    • The main thread remains free to handle other work.
  3. Scalability:
    • Particularly useful in high-throughput systems like financial applications.

Top comments (0)