C# Async/Await Basics
Introduction |
async Keyword |
await Keyword |
Task and Task<T> |
Examples
Introduction to Asynchronous Programming
Asynchronous programming in C# allows your application to perform long-running operations without blocking the main thread. This is crucial for maintaining responsiveness, especially in UI applications and server-side scenarios where blocking can lead to a poor user experience or reduced throughput.
Historically, managing asynchronous operations involved complex patterns like callbacks or the Asynchronous Programming Model (APM). C# 5 introduced the async and await keywords, which dramatically simplify writing and reading asynchronous code, making it look and behave more like synchronous code.
The async Keyword
The async keyword is a modifier that you apply to a method declaration. It indicates that the method contains asynchronous operations and can use the await keyword. A method marked as async does not necessarily run asynchronously from start to finish; it merely enables the use of await within its body.
The return type of an async method can be:
Task: For asynchronous methods that do not return a value.Task<TResult>: For asynchronous methods that return a value of typeTResult.void: Generally discouraged, except for event handlers. Anasync voidmethod cannot be awaited, and exceptions thrown from it can be difficult to handle.- Other awaitable types (less common).
Consider this simple example:
public async Task ProcessDataAsync()
{
// This method is marked as async
Console.WriteLine("Starting data processing...");
await Task.Delay(2000); // Simulate a long-running operation
Console.WriteLine("Data processing complete.");
}
The await Keyword
The await operator is used within an async method to suspend the method's execution until an asynchronous operation (represented by an awaitable, typically a Task or Task<TResult>) completes. When the awaited operation finishes, the method resumes execution from where it left off. If the awaited operation returns a result, that result is returned by the await expression.
Crucially, when await is encountered, control is returned to the caller of the async method. This allows the caller to continue its work, maintaining application responsiveness.
Here's how await works in conjunction with async:
public async Task DownloadFileAsync(string url)
{
Console.WriteLine($"Downloading from {url}...");
// Imagine HttpClient.GetStreamAsync returns a Task<Stream>
using (var stream = await httpClient.GetStreamAsync(url))
{
Console.WriteLine("Download started, awaiting completion...");
// Process the stream here...
await stream.CopyToAsync(fileStream);
}
Console.WriteLine("Download complete.");
}
await keyword can only be used inside a method marked with the async keyword.
Task and Task<TResult>
The System.Threading.Tasks.Task and System.Threading.Tasks.Task<TResult> classes are central to asynchronous programming in .NET. They represent an operation that may complete at a later time.
Task: Represents an asynchronous operation that does not return a value.Task<TResult>: Represents an asynchronous operation that returns a value of typeTResult.
Methods that perform asynchronous work typically return a Task or Task<TResult>. The await keyword unwraps the result of a Task<TResult>, making it behave like a return statement for a synchronous method.
Practical Examples
Example 1: Simulating Network Latency
This example demonstrates downloading content from multiple URLs concurrently, using Task.WhenAll to wait for all operations to complete.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Collections.Generic;
public class AsyncDemo
{
static readonly HttpClient client = new HttpClient();
public static async Task Main(string[] args)
{
Console.WriteLine("Starting asynchronous download operations...");
string[] urls = {
"https://www.example.com",
"https://www.microsoft.com",
"https://www.github.com"
};
List<Task<string>> downloadTasks = new List<Task<string>>();
foreach (var url in urls)
{
downloadTasks.Add(DownloadContentAsync(url));
}
// Wait for all download tasks to complete
string[] results = await Task.WhenAll(downloadTasks);
Console.WriteLine("\nAll downloads completed. Results:");
for (int i = 0; i < urls.Length; i++)
{
Console.WriteLine($"- {urls[i]}: {results[i].Substring(0, Math.Min(results[i].Length, 100))}...");
}
Console.WriteLine("\nMain method finished.");
}
public static async Task<string> DownloadContentAsync(string url)
{
Console.WriteLine($"Initiating download for: {url}");
try
{
string content = await client.GetStringAsync(url);
Console.WriteLine($"Finished download for: {url} ({content.Length} bytes)");
return content;
}
catch (HttpRequestException e)
{
Console.WriteLine($"Error downloading {url}: {e.Message}");
return $"Error: {e.Message}";
}
}
}
Example 2: Async Event Handlers
While async void is generally discouraged, it's commonly used for event handlers where the framework expects a void return type.
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
// Assume button1 is a Button control
button1.Click += Button1_Click;
}
private async void Button1_Click(object sender, EventArgs e)
{
// This is an async void method, used for event handlers
try
{
this.Cursor = Cursors.WaitCursor; // UI responsiveness
button1.Enabled = false;
string result = await PerformLongOperationAsync();
MessageBox.Show($"Operation completed: {result}");
}
catch (Exception ex)
{
MessageBox.Show($"An error occurred: {ex.Message}");
}
finally
{
this.Cursor = Cursors.DefaultCursor;
button1.Enabled = true;
}
}
private async Task<string> PerformLongOperationAsync()
{
// Simulate a CPU-bound or I/O-bound operation
await Task.Delay(3000); // Simulate work
return "Success!";
}
}
async void methods are not easily catchable by the caller. They typically result in the application terminating unless specific exception handling mechanisms are put in place.
Conclusion
The async and await keywords in C# provide a powerful and elegant way to handle asynchronous operations. By understanding how these keywords interact with Task and Task<TResult>, you can write more responsive, scalable, and maintainable applications.
Remember to:
- Mark methods containing
awaitwithasync. - Use
TaskorTask<TResult>as return types for most asynchronous methods. - Avoid
async voidexcept for event handlers. - Handle exceptions appropriately in asynchronous code.