Overview
Asynchronous programming is crucial for building scalable, responsive applications. Understanding async/await deeply is essential for mid-level and senior positions, as it's commonly discussed in technical interviews and affects application performance significantly.
Core Concepts
Task-based Asynchronous Pattern (TAP)
// TAP pattern - async methods return Task or Task<T>
public async Task<Customer> GetCustomerAsync(int id)
{
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
using var command = new SqlCommand("SELECT * FROM Customers WHERE Id = @id", connection);
command.Parameters.AddWithValue("@id", id);
using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return new Customer
{
Id = reader.GetInt32(0),
Name = reader.GetString(1)
};
}
return null;
}
Async All the Way
// Bad - mixing sync and async (causes deadlocks)
public Customer GetCustomer(int id)
{
return GetCustomerAsync(id).Result; // Deadlock risk!
}
// Good - async all the way
public async Task<Customer> GetCustomerAsync(int id)
{
return await _repository.GetCustomerAsync(id);
}
// Usage in ASP.NET Core (async all the way)
[HttpGet("{id}")]
public async Task<ActionResult<Customer>> Get(int id)
{
var customer = await GetCustomerAsync(id);
return Ok(customer);
}
ConfigureAwait
// In library code - don't capture context
public async Task<string> GetDataAsync()
{
var result = await httpClient.GetStringAsync(url)
.ConfigureAwait(false); // Don't capture SynchronizationContext
return ProcessData(result);
}
// In UI/ASP.NET Core - usually don't need ConfigureAwait
// ASP.NET Core doesn't have SynchronizationContext by default
public async Task<IActionResult> GetData()
{
var data = await _service.GetDataAsync(); // No ConfigureAwait needed
return Ok(data);
}
Parallel Execution
// Sequential - slow (takes 3 seconds)
var result1 = await GetDataAsync(); // 1 second
var result2 = await GetDataAsync(); // 1 second
var result3 = await GetDataAsync(); // 1 second
// Parallel - fast (takes 1 second)
var task1 = GetDataAsync();
var task2 = GetDataAsync();
var task3 = GetDataAsync();
await Task.WhenAll(task1, task2, task3);
// Or more concisely
var tasks = new[] { GetDataAsync(), GetDataAsync(), GetDataAsync() };
var results = await Task.WhenAll(tasks);
Bad vs Good Examples
Bad: Blocking on Async
// Bad - causes deadlocks and thread pool starvation
public string GetData()
{
return GetDataAsync().Result; // Blocks thread!
}
public void ProcessData()
{
GetDataAsync().Wait(); // Blocks thread!
}
Good: Async All the Way
// Good - non-blocking
public async Task<string> GetDataAsync()
{
return await httpClient.GetStringAsync(url);
}
public async Task ProcessDataAsync()
{
await GetDataAsync();
}
Bad: Async Void
// Bad - can't be awaited, exceptions crash app
public async void ProcessOrder(int orderId)
{
await _service.ProcessAsync(orderId);
}
Good: Async Task
// Good - can be awaited, exceptions can be caught
public async Task ProcessOrderAsync(int orderId)
{
await _service.ProcessAsync(orderId);
}
// Exception handling works
try
{
await ProcessOrderAsync(123);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order");
}
Interview Tips
Tip 1: Always mention that async/await doesn't create new threads - it uses the thread pool efficiently by not blocking threads during I/O.
Tip 2: Know the difference between CPU-bound work (use Task.Run) and I/O-bound work (use async I/O APIs).
Tip 3: Be ready to explain deadlock scenarios with .Result or .Wait() in UI applications with SynchronizationContext.
Common Interview Questions
-
What's the difference between Task.Run and async/await?
- Task.Run offloads CPU-bound work to thread pool. Async/await handles I/O-bound work without blocking threads.
-
Does async/await create new threads?
- No, it uses existing thread pool threads efficiently. Avoids blocking threads during I/O operations.
-
When should you use ConfigureAwait(false)?
- In library code to avoid capturing SynchronizationContext. Not needed in ASP.NET Core (no SynchronizationContext by default).
-
What's the difference between Task.WhenAll and Task.WaitAll?
- WhenAll is async (returns Task), WaitAll is blocking. WhenAll allows parallel async operations without blocking.
-
Why should you avoid async void?
- Can't be awaited, exceptions can't be caught (crash app), no way to know when complete. Only use for event handlers.
-
How do you handle exceptions in async code?
- Use try-catch around await. For Task.WhenAll, exceptions are aggregated in AggregateException.
-
What's the difference between Task.Delay and Thread.Sleep?
- Task.Delay is async (doesn't block thread), Thread.Sleep blocks the thread. Always use Task.Delay in async code.