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
awaitkeyword. - If an
asyncmethod contains anawaitexpression, control is returned to the caller when the awaited task is not yet completed. - If an
asyncmethod 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/awaitpattern is built upon TAP, where asynchronous operations are represented byTaskorTask<TResult>objects. - Async all the way: If a method calls an asynchronous method, it should typically also be asynchronous, marking itself with
asyncand awaiting the called method. - Cancellation: For long-running operations, implement cancellation using
CancellationTokento allow users to abort operations gracefully. - Error Handling: Exceptions thrown in awaited tasks are rethrown when the await completes, allowing standard
try-catchblocks 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.