How do async and await work in C#?
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.Runto 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 aSynchronizationContext(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.TaskvsThread—Threadis an OS thread;Taskis a promise of a future result that may or may not use a thread, usually scheduled on the thread pool.
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;