Async/Await Programming
Understanding asynchronous programming in C# with async and await
Async/Await Programming
Asynchronous programming is a technique that allows your program to perform multiple operations concurrently without blocking the main thread. This is critical for building responsive applications.
Why Async?
The Problem with Synchronous Code
// Synchronous β blocks the thread
public string GetDataSync()
{
// Thread waits for 2 seconds
var result = httpClient.GetStringAsync("https://api.example.com").Result;
return result;
}
Problems:
- The thread canβt do anything else
- The application βfreezesβ
- Resources are wasted
The Async Solution
// Asynchronous β thread is free
public async Task<string> GetDataAsync()
{
// Thread can do other work while waiting
var result = await httpClient.GetStringAsync("https://api.example.com");
return result;
}
Core Concepts
Task and Task
// Task = async operation with no return value
public async Task DoSomethingAsync()
{
await Task.Delay(1000);
Console.WriteLine("Done!");
}
// Task<T> = async operation with a return value
public async Task<int> CalculateAsync()
{
await Task.Delay(1000);
return 42;
}
async and await Keywords
// async marks a method as asynchronous
// await waits for an async operation to complete
public async Task ProcessAsync()
{
Console.WriteLine("Starting...");
// await = wait until done, but the thread is free
await Task.Delay(2000);
Console.WriteLine("Done!");
}
Common Patterns
Sequential Execution
// Operations run one after another
public async Task SequentialAsync()
{
var result1 = await GetData1Async(); // Wait for completion
var result2 = await GetData2Async(); // Then run this
var result3 = await GetData3Async(); // Then this
// Total time = time1 + time2 + time3
}
Parallel Execution
// All operations run concurrently
public async Task ParallelAsync()
{
// Start all tasks
var task1 = GetData1Async();
var task2 = GetData2Async();
var task3 = GetData3Async();
// Wait for all to complete
await Task.WhenAll(task1, task2, task3);
// Get results
var result1 = task1.Result;
var result2 = task2.Result;
var result3 = task3.Result;
// Total time = longest of the three
}
WhenAll vs WhenAny
// WhenAll β wait for ALL to complete
var results = await Task.WhenAll(task1, task2, task3);
// WhenAny β wait for ANY ONE to complete
var firstCompleted = await Task.WhenAny(task1, task2, task3);
Async in ASP.NET Core
Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _service;
[HttpGet]
public async Task<ActionResult<List<Product>>> GetAll()
{
var products = await _service.GetAllAsync();
return Ok(products);
}
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetById(int id)
{
var product = await _service.GetByIdAsync(id);
if (product == null)
return NotFound();
return Ok(product);
}
}
Service
public class ProductService : IProductService
{
private readonly HttpClient _httpClient;
private readonly AppDbContext _context;
public async Task<Product> GetByIdAsync(int id)
{
return await _context.Products
.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task<List<Product>> GetFromExternalApiAsync()
{
var response = await _httpClient.GetAsync("https://api.example.com/products");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<List<Product>>(content);
}
}
Error Handling
Try-Catch with Async
public async Task<Product?> SafeGetProductAsync(int id)
{
try
{
return await _service.GetByIdAsync(id);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to fetch product {Id}", id);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error");
throw; // re-throw for serious errors
}
}
Exceptions in Task.WhenAll
try
{
await Task.WhenAll(task1, task2, task3);
}
catch (Exception)
{
// Only the first exception is caught
// To get all exceptions:
if (task1.IsFaulted) HandleError(task1.Exception);
if (task2.IsFaulted) HandleError(task2.Exception);
if (task3.IsFaulted) HandleError(task3.Exception);
}
Cancellation
CancellationToken
public async Task<Data> GetDataAsync(CancellationToken cancellationToken = default)
{
// Check if cancelled
cancellationToken.ThrowIfCancellationRequested();
// Pass the token to async operations
var response = await _httpClient.GetAsync(url, cancellationToken);
return await response.Content.ReadFromJsonAsync<Data>(cancellationToken);
}
Using CancellationToken
// In a controller β ASP.NET Core auto-cancels when the request is aborted
[HttpGet]
public async Task<IActionResult> Get(CancellationToken cancellationToken)
{
var data = await _service.GetDataAsync(cancellationToken);
return Ok(data);
}
// Manual timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
var result = await GetDataAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was cancelled or timed out");
}
Best Practices
1. Async All The Way
// β DON'T β blocking on async code
public string GetData()
{
return GetDataAsync().Result; // Deadlock risk!
}
// β
DO β async all the way up
public async Task<string> GetDataAsync()
{
return await FetchDataAsync();
}
2. ConfigureAwait
// In library code
public async Task<string> LibraryMethodAsync()
{
// ConfigureAwait(false) for libraries
var result = await SomeOperationAsync().ConfigureAwait(false);
return result;
}
// In ASP.NET Core, you generally don't need ConfigureAwait
3. Avoid async void
// β DON'T β exceptions can't be caught
public async void BadMethod()
{
await SomethingAsync();
}
// β
DO β use Task
public async Task GoodMethod()
{
await SomethingAsync();
}
// Exception: event handlers
button.Click += async (sender, e) =>
{
await HandleClickAsync();
};
4. ValueTask for Hot Paths
// For operations that are often synchronous
public ValueTask<int> GetCachedValueAsync(string key)
{
if (_cache.TryGetValue(key, out int value))
{
return new ValueTask<int>(value); // No allocation
}
return new ValueTask<int>(FetchAndCacheAsync(key));
}
Common Mistakes
Deadlock
// β Deadlock in ASP.NET Framework (not in Core)
public ActionResult Index()
{
var data = GetDataAsync().Result; // DEADLOCK!
return View(data);
}
Fire and Forget
// β Exceptions are silently lost
public void StartBackgroundWork()
{
DoWorkAsync(); // Exception will never be seen!
}
// β
Better
public async Task StartBackgroundWork()
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Background work failed");
}
}
Summary
| Scenario | Use |
|---|---|
| I/O bound (database, HTTP, file) | async/await |
| CPU bound | Task.Run() or parallel |
| Sequential operations | await one by one |
| Independent operations | Task.WhenAll() |
| Race condition | Task.WhenAny() |
Async/await is one of the most important features in modern C#. Master this concept to build responsive and scalable applications!