C# & ASP.NET Topic
C# Asynchronous Programming (async/await)
Fundamentals of async/await
Basic async/await Syntax
csharp
// Synchronous method
public string DownloadData(string url)
{
using var client = new WebClient();
return client.DownloadString(url); // Blocks thread
}
// Asynchronous method
public async Task<string> DownloadDataAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url); // Doesn't block thread
}
Async Method Patterns
csharp
public class AsyncExamples
{
// Async method without return value
public async Task DoWorkAsync()
{
await Task.Delay(1000);
Console.WriteLine("Work completed");
}
// Async method with return value
public async Task<int> CalculateAsync(int a, int b)
{
await Task.Delay(500);
return a + b;
}
// Async method that returns completed task
public Task<string> GetCachedDataAsync()
{
// If data is already available, return completed task
if (_cache.TryGetValue("data", out string data))
return Task.FromResult(data);
// Otherwise, perform async operation
return FetchDataAsync();
}
// ValueTask for performance (C# 7.0+)
public async ValueTask<int> GetValueAsync()
{
if (_valueAvailable)
return _cachedValue;
return await ComputeValueAsync();
}
}
Task Parallel Library (TPL)
Creating and Running Tasks
csharp
public class TaskCreationExamples
{
// Task.Run for CPU-bound work
public async Task<int> PerformCalculationAsync(int data)
{
// Offload CPU-intensive work to thread pool
return await Task.Run(() =>
{
Thread.Sleep(1000); // Simulate CPU work
return data * data;
});
}
// Task.Factory for more control
public Task<string> ProcessDataAsync(string input)
{
return Task.Factory.StartNew(() =>
{
// Long running task
Thread.Sleep(2000);
return input.ToUpper();
}, TaskCreationOptions.LongRunning);
}
// Task.FromResult for immediate results
public Task<bool> ValidateInputAsync(string input)
{
if (string.IsNullOrEmpty(input))
return Task.FromResult(false);
return Task.FromResult(input.Length > 5);
}
// Creating delayed tasks
public async Task StartWithDelayAsync()
{
await Task.Delay(TimeSpan.FromSeconds(2));
Console.WriteLine("Executed after 2 seconds");
}
}
Task Continuations
csharp
public class TaskContinuationExamples
{
public async Task ContinuationPatterns()
{
// Basic continuation
Task<int> firstTask = Task.Run(() => 42);
Task<string> continuation = firstTask.ContinueWith(previousTask =>
{
int result = previousTask.Result;
return $"The answer is {result}";
});
string message = await continuation;
// Multiple continuations
Task<int> initialTask = Task.Run(() => 10);
initialTask.ContinueWith(t => Console.WriteLine($"Success: {t.Result}"),
TaskContinuationOptions.OnlyOnRanToCompletion);
initialTask.ContinueWith(t => Console.WriteLine($"Failed: {t.Exception}"),
TaskContinuationOptions.OnlyOnFaulted);
// Async continuation with await
Task<int> calculationTask = Task.Run(() => HeavyCalculation());
await calculationTask.ContinueWith(async t =>
{
int result = t.Result;
await SaveResultAsync(result);
}, TaskContinuationOptions.OnlyOnRanToCompletion);
}
}
Concurrency vs Parallelism
Concurrency (Async/Await)
csharp
public class ConcurrencyExamples
{
// Concurrent execution - multiple operations interleaved
public async Task ProcessMultipleOperationsAsync()
{
var task1 = DownloadFileAsync("http://example.com/file1.txt");
var task2 = DownloadFileAsync("http://example.com/file2.txt");
var task3 = DownloadFileAsync("http://example.com/file3.txt");
// All tasks run concurrently
string[] results = await Task.WhenAll(task1, task2, task3);
Console.WriteLine($"Downloaded {results.Length} files");
}
// Sequential vs Concurrent
public async Task SequentialProcessingAsync()
{
// Sequential - one after another
var result1 = await ProcessItemAsync(1); // Waits for completion
var result2 = await ProcessItemAsync(2); // Then starts
var result3 = await ProcessItemAsync(3); // Then starts
}
public async Task ConcurrentProcessingAsync()
{
// Concurrent - all start together
var task1 = ProcessItemAsync(1);
var task2 = ProcessItemAsync(2);
var task3 = ProcessItemAsync(3);
// Wait for all to complete
await Task.WhenAll(task1, task2, task3);
}
// Processing with throttling
public async Task ProcessWithConcurrencyLimitAsync()
{
var items = Enumerable.Range(1, 100);
var tasks = items.Select(ProcessItemAsync);
// Process up to 5 concurrently
var results = await ProcessWithMaxConcurrency(tasks, 5);
}
private async Task<T[]> ProcessWithMaxConcurrency<T>(
IEnumerable<Task<T>> tasks, int maxConcurrency)
{
var allTasks = new List<Task<T>>();
var pending = new Queue<Task<T>>(tasks);
// Start initial batch
while (pending.Count > 0 && allTasks.Count < maxConcurrency)
{
allTasks.Add(pending.Dequeue());
}
// Process as tasks complete
while (allTasks.Count > 0)
{
var completed = await Task.WhenAny(allTasks);
allTasks.Remove(completed);
if (pending.Count > 0)
{
allTasks.Add(pending.Dequeue());
}
}
return await Task.WhenAll(tasks);
}
}
Parallelism (Parallel Class)
csharp
public class ParallelismExamples
{
// CPU-bound parallel processing
public void ProcessDataInParallel()
{
var data = Enumerable.Range(1, 1000).ToArray();
// Parallel.For for data parallelism
Parallel.For(0, data.Length, i =>
{
data[i] = ExpensiveCalculation(data[i]);
});
// Parallel.ForEach for collections
Parallel.ForEach(data, item =>
{
ProcessItem(item);
});
// With options
ParallelOptions options = new ParallelOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount - 1,
CancellationToken = CancellationToken.None
};
Parallel.For(0, data.Length, options, i =>
{
// CPU-intensive work
});
}
// PLINQ (Parallel LINQ)
public void ProcessWithPLinq()
{
var numbers = Enumerable.Range(1, 1000000);
var results = numbers
.AsParallel()
.Where(n => n % 2 == 0)
.Select(n => ExpensiveTransformation(n))
.WithDegreeOfParallelism(4)
.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
.ToArray();
}
// When to use Parallel vs Async
public async Task MixedUsage()
{
// Use Parallel for CPU-bound work
await Task.Run(() =>
{
Parallel.For(0, 100, i =>
{
// CPU-intensive calculations
Thread.SpinWait(1000000);
});
});
// Use Async for I/O-bound work
await DownloadMultipleFilesAsync();
}
}
Task.Run - Proper Usage
Correct Usage Patterns
csharp
public class TaskRunExamples
{
// CORRECT: Use Task.Run for CPU-bound work
public async Task<int> CalculatePrimeAsync(int number)
{
return await Task.Run(() =>
{
// CPU-intensive calculation
return FindNthPrime(number);
});
}
// CORRECT: Mixed I/O and CPU-bound work
public async Task<string> ProcessDataAsync(string input)
{
// I/O-bound operation (no Task.Run needed)
string rawData = await DownloadDataAsync(input);
// CPU-bound processing (use Task.Run)
string processedData = await Task.Run(() => ProcessData(rawData));
// Another I/O-bound operation
await SaveDataAsync(processedData);
return processedData;
}
// INCORRECT: Don't use Task.Run for I/O-bound async methods
public async Task<string> BadExampleAsync(string url)
{
// WRONG - this just adds overhead
return await Task.Run(async () => await _httpClient.GetStringAsync(url));
}
// CORRECT: For I/O-bound, just await directly
public async Task<string> GoodExampleAsync(string url)
{
return await _httpClient.GetStringAsync(url);
}
}
UI Applications with Task.Run
csharp
// In WPF, WinForms, or MAUI applications
public partial class MainWindow : Window
{
private readonly HttpClient _httpClient = new HttpClient();
// Keep UI responsive by offloading CPU work
private async void ProcessButton_Click(object sender, RoutedEventArgs e)
{
try
{
// Show loading indicator
ProgressBar.Visibility = Visibility.Visible;
// Offload CPU-intensive work to background thread
var result = await Task.Run(() => PerformComplexCalculation());
// Back on UI thread - update UI
ResultTextBox.Text = result.ToString();
}
finally
{
ProgressBar.Visibility = Visibility.Hidden;
}
}
// I/O operations don't need Task.Run
private async void DownloadButton_Click(object sender, RoutedEventArgs e)
{
try
{
// No Task.Run needed for async I/O
string data = await _httpClient.GetStringAsync("http://example.com/data");
// We're back on UI thread automatically
ContentTextBox.Text = data;
}
catch (Exception ex)
{
MessageBox.Show($"Download failed: {ex.Message}");
}
}
private int PerformComplexCalculation()
{
// Simulate CPU-intensive work
Thread.Sleep(3000);
return Environment.TickCount;
}
}
Deadlock Avoidance
Common Deadlock Scenarios
csharp
public class DeadlockExamples
{
// DEADLOCK: Blocking on async code
public string GetDataDeadlock()
{
var task = GetDataAsync();
return task.Result; // Blocks, can cause deadlock
}
public async Task<string> GetDataAsync()
{
await Task.Delay(1000);
return "Data";
}
// DEADLOCK: Using Wait() instead of await
public void ProcessDataDeadlock()
{
var task = ProcessDataAsync();
task.Wait(); // Blocks, can cause deadlock
}
// SOLUTION: Use async all the way
public async Task<string> GetDataCorrectAsync()
{
return await GetDataAsync();
}
public async Task ProcessDataCorrectAsync()
{
await ProcessDataAsync();
}
}
ConfigureAwait(false) for Library Code
csharp
public class AsyncLibrary
{
// Use ConfigureAwait(false) in library code
public async Task<string> GetDataAsync()
{
// Don't capture context - can continue on any thread
var data = await DownloadDataAsync().ConfigureAwait(false);
// CPU-bound work
return await Task.Run(() => ProcessData(data));
}
// When you need context, be explicit
public async Task UpdateUIAsync()
{
var data = await GetDataAsync(); // No context captured
// Explicitly switch to UI context if needed
await Application.Current.Dispatcher.InvokeAsync(() =>
{
// Update UI elements
});
}
private async Task<string> DownloadDataAsync()
{
using var client = new HttpClient();
return await client.GetStringAsync("http://example.com")
.ConfigureAwait(false); // Important for libraries
}
}
Deadlock Prevention Patterns
csharp
public class DeadlockPrevention
{
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
// DEADLOCK: Async lock with bad usage
public async Task<string> GetDataWithDeadlockAsync()
{
await _semaphore.WaitAsync();
try
{
// This can deadlock if called from UI thread
return await FetchDataAsync();
}
finally
{
_semaphore.Release();
}
}
// SOLUTION: Use ConfigureAwait(false) in locks
public async Task<string> GetDataSafeAsync()
{
await _semaphore.WaitAsync().ConfigureAwait(false);
try
{
return await FetchDataAsync().ConfigureAwait(false);
}
finally
{
_semaphore.Release();
}
}
// BETTER: Use AsyncLock pattern
public async Task<string> GetDataWithAsyncLock()
{
using (await _asyncLock.LockAsync())
{
return await FetchDataAsync();
}
}
}
// Async-friendly lock
public class AsyncLock
{
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
private readonly Task<IDisposable> _releaser;
public AsyncLock()
{
_releaser = Task.FromResult((IDisposable)new Releaser(this));
}
public Task<IDisposable> LockAsync()
{
var wait = _semaphore.WaitAsync();
return wait.IsCompleted ?
_releaser :
wait.ContinueWith((_, state) => (IDisposable)state,
_releaser.Result, CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
private class Releaser : IDisposable
{
private readonly AsyncLock _lock;
public Releaser(AsyncLock @lock) => _lock = @lock;
public void Dispose() => _lock._semaphore.Release();
}
}
Advanced Patterns
Cancellation Support
csharp
public class CancellationExamples
{
public async Task ProcessWithCancellationAsync()
{
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
await LongRunningOperationAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was cancelled");
}
}
public async Task LongRunningOperationAsync(CancellationToken cancellationToken = default)
{
// Check cancellation token periodically
for (int i = 0; i < 100; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(100, cancellationToken);
await ProcessStepAsync(i, cancellationToken);
}
}
// Cancellation with progress reporting
public async Task<int> ProcessWithProgressAsync(
IProgress<int> progress,
CancellationToken cancellationToken = default)
{
for (int i = 0; i <= 100; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(50, cancellationToken);
progress?.Report(i);
}
return 42;
}
}
Task Combinators
csharp
public class TaskCombinators
{
// Task.WhenAll - wait for all tasks
public async Task ProcessAllAsync()
{
var tasks = new List<Task>
{
ProcessItemAsync(1),
ProcessItemAsync(2),
ProcessItemAsync(3)
};
await Task.WhenAll(tasks);
Console.WriteLine("All tasks completed");
}
// Task.WhenAny - wait for first task to complete
public async Task<string> GetFirstResponseAsync()
{
var tasks = new[]
{
FetchFromSource1Async(),
FetchFromSource2Async(),
FetchFromSource3Async()
};
Task<string> firstCompleted = await Task.WhenAny(tasks);
return await firstCompleted;
}
// Timeout pattern
public async Task<string> GetWithTimeoutAsync()
{
var workTask = LongRunningOperationAsync();
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5));
var completedTask = await Task.WhenAny(workTask, timeoutTask);
if (completedTask == timeoutTask)
throw new TimeoutException("Operation timed out");
return await workTask;
}
// Retry pattern
public async Task<T> RetryAsync<T>(Func<Task<T>> operation, int maxRetries = 3)
{
for (int attempt = 0; attempt < maxRetries; attempt++)
{
try
{
return await operation();
}
catch (Exception ex) when (attempt < maxRetries - 1)
{
Console.WriteLine($"Attempt {attempt + 1} failed: {ex.Message}");
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt))); // Exponential backoff
}
}
throw new InvalidOperationException("All retry attempts failed");
}
}
ValueTask for Performance
csharp
public class ValueTaskExamples
{
private string _cachedData;
private DateTime _cacheTime = DateTime.MinValue;
// Use ValueTask when result is often available synchronously
public ValueTask<string> GetDataAsync()
{
// If data is cached and fresh, return synchronously
if (_cachedData != null && DateTime.UtcNow - _cacheTime < TimeSpan.FromMinutes(5))
{
return new ValueTask<string>(_cachedData);
}
// Otherwise, perform async operation
return new ValueTask<string>(FetchDataAsync());
}
// ValueTask for I/O that might be immediately available
public ValueTask<byte[]> ReadDataAsync(string key)
{
if (_memoryCache.TryGetValue(key, out byte[] data))
{
return new ValueTask<byte[]>(data);
}
return new ValueTask<byte[]>(ReadFromDiskAsync(key));
}
private async Task<string> FetchDataAsync()
{
await Task.Delay(1000); // Simulate I/O
_cachedData = "Fresh Data";
_cacheTime = DateTime.UtcNow;
return _cachedData;
}
}
Real-World Examples
Web API Controller
csharp
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
private readonly ILogger<ProductsController> _logger;
public ProductsController(IProductService productService, ILogger<ProductsController> logger)
{
_productService = productService;
_logger = logger;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
CancellationToken cancellationToken = default)
{
try
{
// Run database queries concurrently
var productsTask = _productService.GetProductsAsync(page, pageSize, cancellationToken);
var totalCountTask = _productService.GetTotalCountAsync(cancellationToken);
await Task.WhenAll(productsTask, totalCountTask);
var products = await productsTask;
var totalCount = await totalCountTask;
Response.Headers.Add("X-Total-Count", totalCount.ToString());
return Ok(products);
}
catch (OperationCanceledException)
{
_logger.LogInformation("Request was cancelled");
return StatusCode(499); // Client Closed Request
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching products");
return StatusCode(500, "An error occurred");
}
}
[HttpPost]
public async Task<ActionResult<Product>> CreateProduct(
[FromBody] CreateProductRequest request,
CancellationToken cancellationToken = default)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
try
{
var product = await _productService.CreateProductAsync(request, cancellationToken);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
}
}
Background Service
csharp
public class DataProcessingService : BackgroundService
{
private readonly ILogger<DataProcessingService> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly PeriodicTimer _timer;
public DataProcessingService(ILogger<DataProcessingService> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
_timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Data Processing Service started");
while (await _timer.WaitForNextTickAsync(stoppingToken))
{
try
{
await ProcessPendingDataAsync(stoppingToken);
}
catch (OperationCanceledException)
{
_logger.LogInformation("Processing was cancelled");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing data");
}
}
_logger.LogInformation("Data Processing Service stopped");
}
private async Task ProcessPendingDataAsync(CancellationToken cancellationToken)
{
using var scope = _serviceProvider.CreateScope();
var processor = scope.ServiceProvider.GetRequiredService<IDataProcessor>();
// Process items with parallelism and cancellation
var pendingItems = await processor.GetPendingItemsAsync(cancellationToken);
var processingTasks = pendingItems.Select(async item =>
{
try
{
await processor.ProcessItemAsync(item, cancellationToken);
_logger.LogInformation("Processed item {ItemId}", item.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing item {ItemId}", item.Id);
}
});
// Limit concurrent processing
await ProcessWithConcurrencyLimit(processingTasks, maxConcurrency: 5, cancellationToken);
}
public override void Dispose()
{
_timer?.Dispose();
base.Dispose();
}
}
Best Practices Summary
Do's and Don'ts
csharp
public class AsyncBestPractices
{
// DO: Use async all the way
public async Task<string> GoodMethodAsync()
{
return await SomeAsyncOperation();
}
// DON'T: Mix async and blocking code
public string BadMethod()
{
return SomeAsyncOperation().Result; // DEADLOCK RISK
}
// DO: Use ConfigureAwait(false) in library code
public async Task LibraryMethodAsync()
{
await SomeOperation().ConfigureAwait(false);
}
// DO: Use cancellation tokens
public async Task ProcessWithCancellationAsync(CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
}
// DON'T: Use async void (except event handlers)
public async void BadVoidAsync() // Exceptions can't be caught
{
await Task.Delay(1000);
}
// DO: Handle exceptions properly
public async Task SafeMethodAsync()
{
try
{
await SomeOperationAsync();
}
catch (Exception ex)
{
// Log and handle appropriately
_logger.LogError(ex, "Operation failed");
throw;
}
}
// DO: Use ValueTask for hot paths
public ValueTask<int> EfficientMethodAsync()
{
if (_cacheValid)
return new ValueTask<int>(_cachedValue);
return new ValueTask<int>(ComputeValueAsync());
}
}
This comprehensive guide covers all aspects of C# asynchronous programming, from basic async/await usage to advanced patterns and real-world applications!
Comments (0)
No comments yet. Be the first to comment!