Advanced Asynchronous Programming
Welcome to the advanced section of our asynchronous programming tutorials. This guide delves into more complex scenarios and patterns for handling asynchronous operations efficiently and robustly in your applications.
Prerequisites
Before proceeding, ensure you have a solid understanding of basic asynchronous concepts, including:
asyncandawaitkeywordsTaskandTask<TResult>ConfigureAwait(false)
If you need a refresher, please review the Networking Basics tutorial which covers these fundamentals.
Understanding the Challenges
While async/await simplifies many asynchronous scenarios, complex applications often present challenges such as:
- Managing multiple concurrent operations.
- Handling and propagating exceptions across multiple tasks.
- Implementing cancellation for long-running operations.
- Dealing with race conditions and deadlocks.
- Optimizing resource utilization.
Patterns for Advanced Async Programming
1. Parallelism with Task.WhenAll
When you need to execute multiple independent asynchronous operations concurrently and wait for all of them to complete, Task.WhenAll is your best friend. It returns a task that completes when all of the supplied tasks have completed.
// Example: Fetching data from multiple sources concurrently
async Task<string[]> FetchMultipleDataAsync()
{
var task1 = GetDataAsync("source1");
var task2 = GetDataAsync("source2");
var task3 = GetDataAsync("source3");
string[] results = await Task.WhenAll(task1, task2, task3);
return results;
}
async Task<string> GetDataAsync(string source)
{
await Task.Delay(TimeSpan.FromSeconds(1)); // Simulate network latency
return $"Data from {source}";
}
2. Handling Multiple Results and Exceptions with Task.WhenAll
Task.WhenAll collects results in the order the tasks were provided. If any of the tasks throw an exception, Task.WhenAll will aggregate these exceptions into an AggregateException.
async Task ProcessItemsAsync()
{
var tasks = new List<Task>();
foreach (var item in _items)
{
tasks.Add(ProcessItemAsync(item));
}
try
{
await Task.WhenAll(tasks);
Console.WriteLine("All items processed successfully.");
}
catch (AggregateException ae)
{
Console.WriteLine("One or more exceptions occurred:");
foreach (var ex in ae.Flatten().InnerExceptions)
{
Console.WriteLine($"- {ex.Message}");
}
}
}
3. Concurrent Operations with Limited Concurrency using SemaphoreSlim
Sometimes you want to run many tasks concurrently, but you need to limit the number of operations running at any given time to avoid overwhelming resources (e.g., database connections, network sockets).
SemaphoreSlim is a lightweight semaphore that can be used for limiting concurrency.
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(4); // Allow up to 4 concurrent operations
async Task ProcessItemsWithConcurrencyLimitAsync()
{
var tasks = new List<Task>();
foreach (var url in _urls)
{
tasks.Add(DownloadUrlAsync(url));
}
await Task.WhenAll(tasks);
Console.WriteLine("All downloads attempted.");
}
async Task DownloadUrlAsync(string url)
{
await _semaphore.WaitAsync(); // Wait for a slot to become available
try
{
Console.WriteLine($"Starting download from {url}");
await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(1, 5))); // Simulate download
Console.WriteLine($"Finished download from {url}");
}
finally
{
_semaphore.Release(); // Release the slot
}
}
4. Implementing Cancellation with CancellationTokenSource
Cancellation is crucial for long-running operations, especially in UI applications where users might want to abort a process. CancellationTokenSource and CancellationToken provide a standard mechanism.
async Task PerformLongRunningOperationAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 100; i++)
{
cancellationToken.ThrowIfCancellationRequested(); // Check for cancellation request
// Simulate work
await Task.Delay(100, cancellationToken);
Console.WriteLine($"Working... step {i}");
}
Console.WriteLine("Long running operation completed.");
}
// How to use it:
async Task RunWithCancellation()
{
using var cts = new CancellationTokenSource();
var longOperationTask = PerformLongRunningOperationAsync(cts.Token);
// Simulate cancelling after 3 seconds
await Task.Delay(3000);
cts.Cancel();
try
{
await longOperationTask;
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was cancelled.");
}
}
Always check for cancellation requests periodically within your asynchronous methods, especially during I/O operations or long loops. Pass the CancellationToken down to any nested asynchronous calls that also support cancellation.
5. Async Streams (IAsyncEnumerable<T>)
For scenarios where you need to process sequences of data asynchronously, especially when the data arrives over time (e.g., from a stream, a network socket, or a database cursor), async streams are the modern solution.
async IAsyncEnumerable<string> StreamDataFromApiAsync()
{
// Simulate receiving data in chunks
await Task.Delay(500);
yield return "Chunk 1";
await Task.Delay(500);
yield return "Chunk 2";
await Task.Delay(500);
yield return "Chunk 3";
}
// How to consume it:
async Task ConsumeStream()
{
await foreach (var dataChunk in StreamDataFromApiAsync())
{
Console.WriteLine($"Received: {dataChunk}");
}
Console.WriteLine("Stream finished.");
}
Best Practices and Considerations
- Use
ConfigureAwait(false)judiciously: In library code, it's generally recommended to useConfigureAwait(false)to avoid potential deadlocks on synchronization contexts. In UI applications, you often need to capture the context. - Handle exceptions: Always wrap your asynchronous calls in
try-catchblocks or useTask.WhenAllwithAggregateExceptionhandling. - Cancellation is key: Design your long-running operations to be cancellable.
- Avoid blocking on async code: Never call
.Resultor.Wait()on a Task from anasyncmethod. This can lead to deadlocks. Instead, useawait. - Resource management: Ensure that resources like connections and streams are properly disposed of, especially in the presence of exceptions or cancellations. Use
usingstatements ortry-finallyblocks. - Performance profiling: Use profiling tools to identify bottlenecks in your asynchronous code.