MSDN Documentation

Asynchronous Programming with C# and async/await

Asynchronous programming in C# allows your application to perform long-running operations without blocking the user interface or other important threads. This is crucial for creating responsive and scalable applications, especially in scenarios involving I/O operations like network requests, file access, or database queries.

The primary mechanism for asynchronous programming in modern C# is the async and await keywords. These keywords simplify the process of writing and consuming asynchronous code, making it look and behave much like synchronous code.

The async Modifier

The async modifier is applied to a method declaration to indicate that the method contains at least one await expression. An async method can be a regular method, an anonymous method, a lambda expression, or a constructor.

Key characteristics of an async method:

  • It can await other asynchronous operations.
  • If an async method does not contain an await keyword, it will execute synchronously.
  • The return type of an async method is typically Task, Task<TResult>, or void. For async-event-handlers, void is the correct return type.

The await Operator

The await operator is used within an async method to pause the execution of the method until the awaited asynchronous operation is complete. While the operation is running, the thread is released, allowing other work to be performed.

When an operation is awaited, the await operator does the following:

  1. It checks if the awaited task is already completed. If so, the method continues execution synchronously.
  2. If the task is not completed, the await operator suspends the execution of the async method.
  3. It registers a continuation to execute when the awaited task completes.
  4. It returns control to the caller of the async method.

Return Types

  • Task: Used for asynchronous methods that do not return a value. It represents an operation that can be waited on.
  • Task<TResult>: Used for asynchronous methods that return a value of type TResult.
  • void: Should be used only for asynchronous event handlers. Other async void methods can lead to unhandled exceptions that are difficult to catch.

Example: Fetching Data Asynchronously

Consider a scenario where you need to download content from a URL. This is a classic I/O-bound operation that should be performed asynchronously.

using System;
using System.Net.Http;
using System.Threading.Tasks;

public class AsyncExample
{
    public static async Task Main(string[] args)
    {
        Console.WriteLine("Starting download...");
        string content = await DownloadContentAsync("https://www.example.com");
        Console.WriteLine("Download complete.");
        Console.WriteLine($"Content length: {content.Length} characters.");
    }

    public static async Task<string> DownloadContentAsync(string url)
    {
        using (HttpClient client = new HttpClient())
        {
            Console.WriteLine("Executing DownloadContentAsync...");
            // The await keyword suspends execution here until the GetStringAsync operation completes.
            // The thread is released during this time.
            string result = await client.GetStringAsync(url);
            Console.WriteLine("DownloadContentAsync finished.");
            return result;
        }
    }
}
Note: When you await a method that returns Task<TResult>, the result of the await expression is of type TResult. If you await a method that returns Task, the result is void.

Exception Handling in Async Methods

Exceptions thrown in an asynchronous method are captured and stored in the returned Task. When you await that task, the exception is re-thrown. This means you can use standard try-catch blocks to handle exceptions from asynchronous operations.

public static async Task ProcessDataAsync()
{
    try
    {
        string data = await FetchRemoteDataAsync();
        // Process data
    }
    catch (HttpRequestException ex)
    {
        Console.WriteLine($"Network error: {ex.Message}");
        // Handle network error
    }
    catch (Exception ex)
    {
        Console.WriteLine($"An unexpected error occurred: {ex.Message}");
        // Handle other errors
    }
}

public static async Task<string> FetchRemoteDataAsync()
{
    // Simulate a potential network error
    throw new HttpRequestException("Failed to connect to the server.");
}
Tip: Always consider the possibility of exceptions and implement appropriate error handling.

Cancellation

For long-running asynchronous operations, it's good practice to provide a mechanism for cancellation. This is typically done using the CancellationToken.

public static async Task DownloadWithCancellationAsync(string url, CancellationToken cancellationToken)
{
    using (HttpClient client = new HttpClient())
    {
        // Register a callback to check for cancellation during the operation
        cancellationToken.ThrowIfCancellationRequested();
        
        // You can also pass the CancellationToken to many awaitable operations
        string result = await client.GetStringAsync(url, cancellationToken);

        cancellationToken.ThrowIfCancellationRequested();
        return result;
    }
}
Important: Ensure that any asynchronous operation you call also supports cancellation if you intend to use CancellationToken effectively.

Benefits of Async/Await

  • Responsiveness: Keeps UI threads free, making applications feel more responsive.
  • Scalability: Efficiently manages resources, especially for I/O-bound operations, allowing servers to handle more concurrent requests.
  • Simplicity: Makes asynchronous code easier to write, read, and maintain compared to older asynchronous patterns (e.g., callbacks, APM, EAP).

Mastering async and await is fundamental for modern C# development, enabling you to build efficient and robust applications.