Async and Await in C#

The async and await keywords in C# provide a straightforward way to write asynchronous code. Asynchronous programming is essential for building responsive applications, especially those that perform I/O operations or long-running tasks, as it prevents the application from freezing.

Understanding Asynchronous Programming

Traditionally, long-running operations on a UI thread would block the user interface, leading to a poor user experience. Asynchronous programming allows these operations to execute in the background, freeing up the UI thread to remain responsive. The async and await keywords simplify this process by allowing you to write asynchronous code that looks and behaves in many ways like synchronous code.

The async Keyword

You mark a method with the async modifier to indicate that it might contain one or more await expressions. An async method has the following characteristics:

Example of an async method:


public async Task<int> CalculateResultAsync()
{
    await Task.Delay(1000); // Simulate a long-running operation
    return 42;
}
            

The await Operator

The await operator is used within an async method to pause the execution of the method until an awaited asynchronous operation completes. While the operation is running, the thread that executed the await is released to do other work, such as processing UI messages.

The operation that is awaited must be an "awaitable" type, which typically means it has a GetAwaiter method. Task and Task<TResult> are common awaitable types.

How await Works:

  1. When an await expression is encountered, the method's execution is suspended.
  2. Control is returned to the caller of the async method.
  3. If the awaited operation is already complete, the method resumes immediately.
  4. If the awaited operation is not complete, the thread is typically released. When the awaited operation completes, the rest of the async method is scheduled to run, often on the original synchronization context if one exists (e.g., on the UI thread).

Working with Task and Task<TResult>

Task represents an asynchronous operation that does not return a value, while Task<TResult> represents an asynchronous operation that returns a value of type TResult.

Example using await with Task<TResult>:


public async Task ProcessDataAsync()
{
    Console.WriteLine("Starting data processing...");
    int result = await FetchDataAsync(); // Await the result of an async operation
    Console.WriteLine($"Data fetched. Result: {result}");
    // ... further processing with the result
}

public async Task<int> FetchDataAsync()
{
    await Task.Delay(2000); // Simulate network latency
    return 123;
}
            

Best Practices and Considerations

Tip: Always await a Task unless you have a specific reason not to. If you don't await a Task returned by an async method, you might lose exceptions.
Note: For UI applications (like WPF, UWP, or WinForms), use ConfigureAwait(false) on awaited tasks when the code after the await doesn't need to run on the UI thread. This can improve performance and prevent deadlocks.

await SomeLongRunningOperationAsync().ConfigureAwait(false);
                
Warning: Avoid using async void for methods that are not event handlers. Exceptions thrown from async void methods cannot be caught by the caller and will typically crash the application.

Understanding Synchronization Context

The synchronization context determines where the continuation of an asynchronous method will run. In UI applications, the synchronization context is typically tied to the UI thread. In server-side applications, there might not be a synchronization context or it might behave differently.

Common Scenarios

Further Reading