Async and Await in .NET Core
Introduction to Asynchronous Programming
Asynchronous programming allows your application to perform long-running operations without blocking the main thread. This is crucial for maintaining responsiveness, especially in UI applications, and for improving scalability in server-side applications. In .NET, the async
and await
keywords provide a powerful and elegant way to write asynchronous code.
The async
Modifier
The async
modifier is applied to a method, lambda expression, or anonymous method declaration. It indicates that the method is asynchronous and may use the await
keyword. Methods marked with async
have special behavior:
- They can use the
await
keyword. - If an
async
method contains anawait
expression, control is returned to the caller when the awaited task is not yet completed. - If an
async
method completes without awaiting anything, or if it completes after an awaited task has completed, it returns normally.
The return type of an async
method is typically Task
, Task<TResult>
, or void
. Returning void
is generally discouraged for asynchronous methods, as it makes error handling and composition difficult; Task
or Task<TResult>
are preferred.
The await
Operator
The await
operator is applied to a task (typically a Task
or Task<TResult>
) that represents an asynchronous operation. When execution reaches an await
expression:
- If the task has already completed, the method continues execution synchronously.
- If the task has not completed, the method pauses execution at that point. The control is returned to the caller of the asynchronous method. Once the awaited task completes, execution resumes in the asynchronous method from where it left off.
Example: Fetching Data Asynchronously
C# Code Example
using System;
using System.Net.Http;
using System.Threading.Tasks;
public class DataFetcher
{
public async Task<string> FetchDataFromUrlAsync(string url)
{
using (HttpClient client = new HttpClient())
{
Console.WriteLine($"Starting to fetch data from {url}...");
string data = await client.GetStringAsync(url);
Console.WriteLine($"Finished fetching data from {url}.");
return data;
}
}
public async Task ProcessDataAsync()
{
string apiUrl = "https://jsonplaceholder.typicode.com/todos/1";
try
{
string result = await FetchDataFromUrlAsync(apiUrl);
Console.WriteLine("Data processed successfully.");
// Process the 'result' string here
Console.WriteLine($"Received: {result.Substring(0, Math.Min(result.Length, 100))}...");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
}
public static async Task Main(string[] args)
{
DataFetcher fetcher = new DataFetcher();
await fetcher.ProcessDataAsync();
Console.WriteLine("Main method finished.");
}
}
Key Concepts and Best Practices
- The Task Asynchronous Pattern (TAP): The
async
/await
pattern is built upon TAP, where asynchronous operations are represented byTask
orTask<TResult>
objects. - Async all the way: If a method calls an asynchronous method, it should typically also be asynchronous, marking itself with
async
and awaiting the called method. - Cancellation: For long-running operations, implement cancellation using
CancellationToken
to allow users to abort operations gracefully. - Error Handling: Exceptions thrown in awaited tasks are rethrown when the await completes, allowing standard
try-catch
blocks to work with asynchronous code. - Synchronization Context: Be mindful of the synchronization context when using
ConfigureAwait(false)
, especially in library code, to avoid deadlocks.
ConfigureAwait(false)
in library code can improve performance and prevent deadlocks by not capturing and resuming on the original synchronization context.
Common Scenarios
- I/O Operations: Reading from files, making network requests, database queries.
- CPU-Bound Operations: Offloading heavy computations to a background thread or task pool using
Task.Run()
. - User Interface Responsiveness: Keeping the UI thread free from blocking operations.