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:
- It is declared with the
async
modifier. - It typically contains at least one
await
expression. If it doesn't, it runs synchronously. - The return type of an
async
method is usuallyTask
,Task<TResult>
, orvoid
(though returningvoid
is generally discouraged except for event handlers).
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:
- When an
await
expression is encountered, the method's execution is suspended. - Control is returned to the caller of the
async
method. - If the awaited operation is already complete, the method resumes immediately.
- 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
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);
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
- I/O Operations: Reading/writing files, making network requests, database queries.
- Long-Running Computations: Complex calculations that could block the UI.
- Parallel Processing: While
async/await
is primarily about concurrency, it can be used in conjunction with parallel programming constructs.