Async/Await Fundamentals

Master asynchronous programming in C# with async/await to build responsive, scalable applications

C#
Async
Concurrency
Performance
20 min read

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

  1. 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.
  2. Does async/await create new threads?

    • No, it uses existing thread pool threads efficiently. Avoids blocking threads during I/O operations.
  3. When should you use ConfigureAwait(false)?

    • In library code to avoid capturing SynchronizationContext. Not needed in ASP.NET Core (no SynchronizationContext by default).
  4. 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.
  5. 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.
  6. How do you handle exceptions in async code?

    • Use try-catch around await. For Task.WhenAll, exceptions are aggregated in AggregateException.
  7. 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.