C# Async Programming
Overview
Asynchronous programming allows you to write non-blocking code that can handle multiple operations concurrently. C# provides powerful async/await keywords for this purpose.
Basic Async Concepts
Synchronous vs Asynchronous
csharp
public class SynchronousExample
{
public void DownloadFiles()
{
// Synchronous - blocks the thread
string file1 = DownloadFile("file1.txt");
string file2 = DownloadFile("file2.txt");
string file3 = DownloadFile("file3.txt");
Console.WriteLine("All files downloaded");
}
private string DownloadFile(string filename)
{
// Simulate file download (blocks for 2 seconds)
System.Threading.Thread.Sleep(2000);
return $"Content of {filename}";
}
}
public class AsynchronousExample
{
public async Task DownloadFilesAsync()
{
// Asynchronous - doesn't block the thread
Task<string> task1 = DownloadFileAsync("file1.txt");
Task<string> task2 = DownloadFileAsync("file2.txt");
Task<string> task3 = DownloadFileAsync("file3.txt");
// Wait for all tasks to complete
string[] results = await Task.WhenAll(task1, task2, task3);
Console.WriteLine("All files downloaded");
}
private async Task<string> DownloadFileAsync(string filename)
{
// Simulate async file download
await Task.Delay(2000);
return $"Content of {filename}";
}
}Async and Await Keywords
csharp
public class AsyncAwaitBasics
{
// Async method that returns Task
public async Task<string> GetDataAsync()
{
Console.WriteLine("Starting data retrieval...");
// Await an async operation
string data = await FetchDataFromServerAsync();
Console.WriteLine("Data retrieved successfully");
return data;
}
// Async method that returns Task<T>
public async Task<int> CalculateAsync(int a, int b)
{
Console.WriteLine("Starting calculation...");
// Simulate async calculation
await Task.Delay(1000);
int result = a + b;
Console.WriteLine($"Calculation complete: {result}");
return result;
}
// Async void method (use with caution)
public async void FireAndForget()
{
try
{
await SomeAsyncOperation();
}
catch (Exception ex)
{
Console.WriteLine($"Error in fire-and-forget: {ex.Message}");
}
}
private async Task<string> FetchDataFromServerAsync()
{
await Task.Delay(2000);
return "Server data";
}
private async Task SomeAsyncOperation()
{
await Task.Delay(500);
Console.WriteLine("Async operation completed");
}
}Task-Based Asynchronous Pattern (TAP)
Creating Async Methods
csharp
public class TaskBasedAsync
{
// Method that returns Task
public async Task ProcessDataAsync()
{
Console.WriteLine("Processing data...");
await Task.Delay(1000);
Console.WriteLine("Data processed");
}
// Method that returns Task<T>
public async Task<int> CalculateSumAsync(int a, int b)
{
await Task.Delay(500);
return a + b;
}
// Method that uses Task.Run for CPU-bound work
public async Task<long> CalculateFactorialAsync(int n)
{
return await Task.Run(() =>
{
long result = 1;
for (int i = 2; i <= n; i++)
{
result *= i;
}
return result;
});
}
}Task Creation and Execution
csharp
public class TaskCreation
{
public async Task DemonstrateTaskCreation()
{
// Create and start a task
Task task1 = Task.Run(() =>
{
Console.WriteLine("Task 1 running");
System.Threading.Thread.Sleep(1000);
Console.WriteLine("Task 1 completed");
});
// Create task with return value
Task<int> task2 = Task.Run(() =>
{
Console.WriteLine("Task 2 running");
return 42;
});
// Create task using Task.Factory
Task<string> task3 = Task.Factory.StartNew(() =>
{
Console.WriteLine("Task 3 running");
return "Hello from Task 3";
});
// Wait for tasks to complete
await task1;
int result2 = await task2;
string result3 = await task3;
Console.WriteLine($"Task 2 result: {result2}");
Console.WriteLine($"Task 3 result: {result3}");
}
}Advanced Async Patterns
Multiple Awaits
csharp
public class MultipleAwaits
{
// Sequential execution
public async Task SequentialExecution()
{
Console.WriteLine("Starting sequential execution...");
string result1 = await GetDataAsync("source1");
Console.WriteLine($"Got: {result1}");
string result2 = await GetDataAsync("source2");
Console.WriteLine($"Got: {result2}");
string result3 = await GetDataAsync("source3");
Console.WriteLine($"Got: {result3}");
Console.WriteLine("Sequential execution complete");
}
// Concurrent execution
public async Task ConcurrentExecution()
{
Console.WriteLine("Starting concurrent execution...");
Task<string> task1 = GetDataAsync("source1");
Task<string> task2 = GetDataAsync("source2");
Task<string> task3 = GetDataAsync("source3");
string[] results = await Task.WhenAll(task1, task2, task3);
Console.WriteLine($"Got: {results[0]}");
Console.WriteLine($"Got: {results[1]}");
Console.WriteLine($"Got: {results[2]}");
Console.WriteLine("Concurrent execution complete");
}
// Wait for any task to complete
public async Task WaitForAny()
{
Task<string> task1 = GetDataAsync("fast");
Task<string> task2 = GetDataAsync("slow");
Task<string> task3 = GetDataAsync("medium");
Task<string> firstCompleted = await Task.WhenAny(task1, task2, task3);
string result = await firstCompleted;
Console.WriteLine($"First completed: {result}");
}
private async Task<string> GetDataAsync(string source)
{
int delay = source switch
{
"fast" => 1000,
"medium" => 2000,
"slow" => 3000,
_ => 1500
};
await Task.Delay(delay);
return $"Data from {source}";
}
}Exception Handling in Async Methods
csharp
public class AsyncExceptionHandling
{
public async Task HandleExceptions()
{
try
{
await MethodThatThrowsAsync();
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Invalid operation: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"General exception: {ex.Message}");
}
}
public async Task HandleMultipleExceptions()
{
Task task1 = MethodThatThrowsAsync("Task 1");
Task task2 = MethodThatThrowsAsync("Task 2");
Task task3 = SuccessfulMethodAsync("Task 3");
try
{
await Task.WhenAll(task1, task2, task3);
}
catch (Exception ex)
{
// Only gets the first exception
Console.WriteLine($"Exception in WhenAll: {ex.Message}");
// To get all exceptions:
if (ex is AggregateException ae)
{
foreach (var innerEx in ae.InnerExceptions)
{
Console.WriteLine($"Inner exception: {innerEx.Message}");
}
}
}
}
private async Task MethodThatThrowsAsync(string taskName)
{
await Task.Delay(1000);
throw new InvalidOperationException($"Error in {taskName}");
}
private async Task SuccessfulMethodAsync(string taskName)
{
await Task.Delay(1500);
Console.WriteLine($"{taskName} completed successfully");
}
}Cancellation Support
csharp
public class AsyncCancellation
{
public async Task DemonstrateCancellation()
{
using CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// Start a long-running operation
Task longRunningTask = LongRunningOperationAsync(token);
// Cancel after 3 seconds
Task.Delay(3000).ContinueWith(_ => cts.Cancel());
try
{
await longRunningTask;
Console.WriteLine("Operation completed successfully");
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was cancelled");
}
}
public async Task LongRunningOperationAsync(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();
Console.WriteLine($"Processing step {i + 1}/10");
await Task.Delay(1000, token);
}
Console.WriteLine("Long-running operation completed");
}
public async Task CancellationWithTimeout()
{
using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
await LongRunningOperationAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation timed out after 5 seconds");
}
}
}Async Streams (C# 8.0+)
IAsyncEnumerable
csharp
public class AsyncStreams
{
// Method that returns IAsyncEnumerable<T>
public async IAsyncEnumerable<int> GenerateNumbersAsync(int count)
{
for (int i = 1; i <= count; i++)
{
await Task.Delay(500); // Simulate async work
yield return i;
}
}
// Method that consumes IAsyncEnumerable<T>
public async Task ConsumeAsyncStream()
{
await foreach (int number in GenerateNumbersAsync(10))
{
Console.WriteLine($"Received: {number}");
}
}
// Real-world example: Reading data from database
public async IAsyncEnumerable<Customer> GetCustomersAsync()
{
const int batchSize = 100;
int offset = 0;
while (true)
{
List<Customer> batch = await GetCustomerBatchAsync(offset, batchSize);
if (batch.Count == 0)
yield break;
foreach (Customer customer in batch)
{
yield return customer;
}
offset += batchSize;
}
}
private async Task<List<Customer>> GetCustomerBatchAsync(int offset, int size)
{
// Simulate database query
await Task.Delay(100);
return Enumerable.Range(offset, Math.Min(size, 10))
.Select(i => new Customer { Id = i, Name = $"Customer {i}" })
.ToList();
}
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
}Practical Examples
Async File Operations
csharp
public class AsyncFileOperations
{
public async Task ProcessFilesAsync(string[] filePaths)
{
List<Task<string>> readTasks = new List<Task<string>>();
// Start all file reads concurrently
foreach (string filePath in filePaths)
{
Task<string> readTask = ReadFileAsync(filePath);
readTasks.Add(readTask);
}
// Wait for all files to be read
string[] contents = await Task.WhenAll(readTasks);
// Process all contents
for (int i = 0; i < contents.Length; i++)
{
Console.WriteLine($"File {filePaths[i]}: {contents[i].Length} characters");
}
}
private async Task<string> ReadFileAsync(string filePath)
{
using StreamReader reader = new StreamReader(filePath);
return await reader.ReadToEndAsync();
}
public async Task WriteFilesAsync(Dictionary<string, string> fileContents)
{
List<Task> writeTasks = new List<Task>();
foreach (var kvp in fileContents)
{
Task writeTask = WriteFileAsync(kvp.Key, kvp.Value);
writeTasks.Add(writeTask);
}
await Task.WhenAll(writeTasks);
}
private async Task WriteFileAsync(string filePath, string content)
{
using StreamWriter writer = new StreamWriter(filePath);
await writer.WriteAsync(content);
}
}Async HTTP Operations
csharp
using System.Net.Http;
public class AsyncHttpOperations
{
private readonly HttpClient _httpClient;
public AsyncHttpOperations()
{
_httpClient = new HttpClient();
}
public async Task<string> DownloadContentAsync(string url)
{
try
{
HttpResponseMessage response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException ex)
{
Console.WriteLine($"HTTP request failed: {ex.Message}");
return string.Empty;
}
}
public async Task<List<string>> DownloadMultipleUrlsAsync(string[] urls)
{
List<Task<string>> downloadTasks = new List<Task<string>>();
foreach (string url in urls)
{
Task<string> downloadTask = DownloadContentAsync(url);
downloadTasks.Add(downloadTask);
}
string[] results = await Task.WhenAll(downloadTasks);
return results.ToList();
}
public async IAsyncEnumerable<string> StreamUrlsAsync(string[] urls)
{
foreach (string url in urls)
{
string content = await DownloadContentAsync(url);
yield return content;
}
}
}Async Database Operations
csharp
public class AsyncDatabaseOperations
{
public async Task<List<Product>> GetProductsAsync()
{
// Simulate database query
await Task.Delay(100);
return new List<Product>
{
new Product { Id = 1, Name = "Laptop", Price = 999.99m },
new Product { Id = 2, Name = "Mouse", Price = 29.99m },
new Product { Id = 3, Name = "Keyboard", Price = 79.99m }
};
}
public async Task<Product> GetProductAsync(int id)
{
var products = await GetProductsAsync();
return products.FirstOrDefault(p => p.Id == id);
}
public async Task<bool> SaveProductAsync(Product product)
{
// Simulate database save
await Task.Delay(200);
Console.WriteLine($"Product {product.Name} saved successfully");
return true;
}
public async Task<List<Product>> SearchProductsAsync(string searchTerm)
{
var products = await GetProductsAsync();
return products.Where(p =>
p.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
.ToList();
}
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}Best Practices
Async/Await Guidelines
csharp
// Good: Use async all the way down
public async Task<string> GetDataAsync()
{
var data = await FetchFromDatabaseAsync();
return ProcessData(data);
}
// Bad: Mixing sync and async
public string GetDataBad()
{
var data = FetchFromDatabaseAsync().Result; // Blocking!
return ProcessData(data);
}
// Good: ConfigureAwait for library code
public async Task<string> GetDataAsync()
{
var data = await FetchFromDatabaseAsync().ConfigureAwait(false);
return ProcessData(data);
}Exception Handling Best Practices
csharp
// Good: Handle exceptions properly
public async Task ProcessDataAsync()
{
try
{
await SomeOperationAsync();
}
catch (SpecificException ex)
{
// Handle specific exception
LogError(ex);
}
catch (Exception ex)
{
// Handle general exception
LogError(ex);
throw; // Re-throw if needed
}
}
// Good: Handle Task.WhenAll exceptions
public async Task ProcessMultipleOperationsAsync()
{
var tasks = new[] { Op1Async(), Op2Async(), Op3Async() };
try
{
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
// Handle first exception
LogError(ex);
}
// Or handle all exceptions
foreach (var task in tasks)
{
if (task.IsFaulted)
{
LogError(task.Exception);
}
}
}Performance Considerations
csharp
// Good: Use ValueTask for frequently completed operations
public ValueTask<int> GetValueAsync()
{
if (_cachedValue.HasValue)
return new ValueTask<int>(_cachedValue.Value);
return new ValueTask<int>(LoadValueAsync());
}
// Good: Avoid async void except for event handlers
public event EventHandler SomethingHappened;
protected virtual void OnSomethingHappened()
{
SomethingHappened?.Invoke(this, EventArgs.Empty);
}
// Good: Use ConfigureAwait(false) in library code
public async Task<string> GetDataAsync()
{
var result = await SomeOperationAsync().ConfigureAwait(false);
return result;
}Summary
In this chapter, you learned:
- Basic async/await concepts and syntax
- Task-based asynchronous pattern (TAP)
- Multiple async operations and coordination
- Exception handling in async methods
- Cancellation support with CancellationToken
- Async streams with IAsyncEnumerable
- Practical examples with files, HTTP, and databases
- Best practices and performance considerations
Asynchronous programming is essential for building responsive applications that can handle multiple operations efficiently. Mastering async/await will help you write better, more scalable C# applications.