Skip to content

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.

Content is for learning and research only.