How do async and await work in C#?

C# Async & Concurrency Intermediate 2–3 Yrs3–5 Yrs Conceptual 2 min read

Quick Answer

async/await let you write non-blocking asynchronous code that reads sequentially. await on an incomplete Task returns control to the caller and registers a continuation; when the awaited operation finishes, the rest of the method resumes. It does not create a thread — for I/O, no thread is used while waiting; it frees the current thread to do other work.

async and await let me write asynchronous code that still reads top to bottom. When I await a task that isn't finished, the method returns to its caller and the framework registers the rest of the method as a continuation. When the operation completes, that continuation runs and the method carries on. The key thing people get wrong is thinking await spins up a thread — it doesn't. For I/O like a database call, while we're waiting there's no thread at all; the current thread goes back to the pool. On a web server that's why async improves throughput under load — those threads go and serve other requests. The rules I follow are: async all the way down, never block on it with .Result, and use ConfigureAwait(false) in library code.

The compiler turns an async method into a state machine. At the first await on an unfinished task, the method returns a Task to its caller. The awaited operation (a socket read, a DB call) completes via an I/O completion callback, and the continuation — the remaining code — is scheduled to run.

Points interviewers probe:

  • “Async doesn't mean another thread.” For I/O-bound work, awaiting releases the thread; nothing is blocked or spinning. For CPU-bound work you still need Task.Run to use a thread-pool thread.
  • Scalability — on a server, freed request threads handle other requests, so throughput goes up under load even though a single request isn't faster.
  • Async all the way — don't block on async with .Result / .Wait(); in a context with a SynchronizationContext (classic ASP.NET, UI) that deadlocks. ASP.NET Core has no such context, but blocking still wastes a thread.
  • ConfigureAwait(false) — in library code, don't force the continuation back onto the original context.
  • Task vs Thread — Thread is an OS thread; Task is a promise of a future result that may or may not use a thread, usually scheduled on the thread pool.

C# — async I/O
public async Task<Customer?> GetCustomerAsync(int id)
{
    // no thread is held while the DB responds
    await using var conn = new SqlConnection(_connString);
    await conn.OpenAsync();

    return await conn.QuerySingleOrDefaultAsync<Customer>(
        "SELECT * FROM Customers WHERE Id = @id", new { id });
}

// DON'T: blocks a thread and can deadlock in some contexts
var c = GetCustomerAsync(1).Result;