MSDN Documentation

C# Async and Await: Advanced Patterns

This document explores advanced patterns and considerations when working with asynchronous programming in C#, building upon the foundational knowledge of async and await.

1. Task-Returning Methods and Exceptions

Understanding how exceptions are propagated through Task objects is crucial for robust asynchronous code. When an asynchronous operation initiated by a Task throws an exception, that exception is captured within the Task itself. When you await this Task, the exception is re-thrown on the awaiting thread.

Note: Unhandled exceptions in asynchronous code can lead to application termination if not properly caught. Always use try-catch blocks around await expressions that might throw exceptions.

using System;
using System.Threading.Tasks;

public class AdvancedAsync
{
    public async Task FetchDataAsync()
    {
        await Task.Delay(100); // Simulate I/O
        throw new InvalidOperationException("Failed to fetch data.");
    }

    public async Task ProcessDataAsync()
    {
        try
        {
            string data = await FetchDataAsync();
            Console.WriteLine("Data fetched: " + data);
        }
        catch (InvalidOperationException ex)
        {
            Console.WriteLine("Error processing data: " + ex.Message);
        }
        catch (Exception ex)
        {
            Console.WriteLine("An unexpected error occurred: " + ex.Message);
        }
    }
}
            

2. Configuring Task Continuations

The Task.ContinueWith() method allows you to schedule a continuation that runs after a task completes. You can configure its behavior using TaskContinuationOptions, such as specifying when the continuation should run (e.g., only on success, only on failure, etc.) and whether it should run synchronously.


public async Task DoWorkAndLogAsync()
{
    Task longRunningTask = Task.Run(() => {
        // Simulate long-running operation
        System.Threading.Thread.Sleep(2000);
        Console.WriteLine("Long running operation completed.");
    });

    // Schedule a continuation to run only if the task completes successfully
    longRunningTask.ContinueWith(task =>
    {
        if (task.Status == TaskStatus.RanToCompletion)
        {
            Console.WriteLine("Operation succeeded. Logging success.");
            // Log success
        }
    }, TaskContinuationOptions.OnlyOnRanToCompletion);

    // Schedule a continuation to run only if the task faults
    longRunningTask.ContinueWith(task =>
    {
        Console.WriteLine($"Operation failed with exception: {task.Exception?.InnerException?.Message}");
        // Log error
    }, TaskContinuationOptions.OnlyOnFaulted);

    await longRunningTask; // Await the main task to ensure it finishes
}
            

3. Cancellation Tokens

Cancellation is a critical aspect of managing long-running asynchronous operations. CancellationToken and CancellationTokenSource provide a mechanism to signal cancellation requests to asynchronous operations, allowing them to gracefully stop their work.

Tip: Always pass CancellationToken as the last parameter to asynchronous methods that support cancellation.

public async Task DownloadFileAsync(string url, string outputPath, CancellationToken cancellationToken)
{
    using (var httpClient = new HttpClient())
    {
        using (var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
        {
            response.EnsureSuccessStatusCode();

            using (var streamToReadFrom = await response.Content.ReadAsStreamAsync())
            using (var streamToWriteTo = new FileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
            {
                var buffer = new byte[8192];
                long totalRead = 0;
                long totalLength = response.Content.Headers.ContentLength ?? -1;

                while (true)
                {
                    // Check for cancellation *before* reading
                    cancellationToken.ThrowIfCancellationRequested();

                    int bytesRead = await streamToReadFrom.ReadAsync(buffer, 0, buffer.Length, cancellationToken);

                    if (bytesRead == 0)
                    {
                        break; // End of stream
                    }

                    await streamToWriteTo.WriteAsync(buffer, 0, bytesRead, cancellationToken);
                    totalRead += bytesRead;

                    // Optionally report progress and check cancellation again
                    // ReportProgress(totalRead, totalLength);
                }
            }
        }
    }
}

// Example usage:
public async Task StartDownloadWithCancellation()
{
    using (var cts = new CancellationTokenSource())
    {
        var downloadTask = DownloadFileAsync("http://example.com/largefile.zip", "downloaded.zip", cts.Token);

        // Simulate cancelling after a few seconds
        await Task.Delay(5000);
        Console.WriteLine("Requesting cancellation...");
        cts.Cancel();

        try
        {
            await downloadTask;
            Console.WriteLine("Download completed successfully.");
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Download was cancelled.");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Download failed: {ex.Message}");
        }
    }
}
            

4. IAsyncEnumerable and Asynchronous Streams

Introduced in C# 8.0, IAsyncEnumerable<T> and the await foreach syntax provide a way to process sequences of data asynchronously, useful for scenarios like streaming large datasets or handling events over time without blocking.


using System.Collections.Generic;

public static class AsyncStreamExample
{
    public static async IAsyncEnumerable GenerateNumbersAsync()
    {
        for (int i = 0; i < 10; i++)
        {
            await Task.Delay(100); // Simulate async work
            yield return i;
        }
    }

    public static async Task ConsumeStreamAsync()
    {
        Console.WriteLine("Consuming asynchronous stream:");
        await foreach (var number in GenerateNumbersAsync())
        {
            Console.WriteLine($"Received: {number}");
        }
        Console.WriteLine("Finished consuming stream.");
    }
}
            

5. Synchronization Context and Deadlocks

When dealing with UI applications or ASP.NET (pre-Core), the SynchronizationContext can be a source of deadlocks if not handled carefully. By default, await captures the current context and attempts to resume on it. For library code, it's often recommended to use ConfigureAwait(false) to avoid capturing the context and prevent potential deadlocks.

Warning: Use ConfigureAwait(false) judiciously. It's appropriate for library code that shouldn't assume a specific UI or web context, but should be avoided in UI event handlers or code that *must* run on a specific context.

// In a library class:
public async Task<string> GetDataFromExternalServiceAsync()
{
    using (var httpClient = new HttpClient())
    {
        // ConfigureAwait(false) prevents capturing the calling context,
        // thus avoiding potential deadlocks in some scenarios.
        var response = await httpClient.GetStringAsync("http://api.example.com/data").ConfigureAwait(false);
        return response;
    }
}
            

6. Task-Returning vs. `void` Returning Async Methods

Asynchronous methods should generally return Task or Task<TResult>. Returning void from an async method is discouraged as it makes it impossible to track the completion or exceptions of the operation. The primary exception is for event handlers, where the signature is fixed to void.

7. Using Parallelism with `Parallel.ForEachAsync`

For scenarios where you need to perform multiple asynchronous operations concurrently on a collection, Parallel.ForEachAsync (available from .NET 6) offers a convenient and efficient way to achieve this.


using System.Collections.Generic;
using System.Threading.Tasks;

public class ParallelAsyncExample
{
    public static async Task ProcessItemsInParallelAsync(IEnumerable<string> items)
    {
        await Parallel.ForEachAsync(items, async (item, cancellationToken) =>
        {
            Console.WriteLine($"Processing item: {item} on thread {System.Threading.Thread.CurrentThread.ManagedThreadId}");
            await Task.Delay(500, cancellationToken); // Simulate async work for each item
            Console.WriteLine($"Finished processing item: {item}");
        });
        Console.WriteLine("All items processed in parallel.");
    }
}
            

Mastering these advanced patterns will enable you to write more efficient, responsive, and robust asynchronous applications in C#.