How it works
The explanation, with examples you can run.
The deadlock
// WPF or WinForms: this freezes the windowvoid Button_Click(object sender, EventArgs e){ string html = LoadAsync().Result; // blocks the UI thread label.Text = html;} async Task<string> LoadAsync(){ string s = await http.GetStringAsync(url); // wants to resume on the UI thread return s.ToUpperInvariant();}The UI thread is blocked inside .Result, waiting for LoadAsync. When the download finishes, the continuation is posted to the UI thread's queue, which nobody is processing. Neither can proceed. The fix is to make the handler async void (event handlers are the one place that is right) and await LoadAsync().
ASP.NET Core and console apps have no context: in this path's .NET 10 runs, SynchronizationContext.Current was null in a console app and in a web app, and the ConfigureAwait FAQ confirms that ASP.NET Core doesn't install one for requests either. So this exact deadlock can't occur there, but blocking still causes thread-pool starvation, and the same library code may be called from a UI app.
ConfigureAwait(false)
await task.ConfigureAwait(false) tells the await not to post back to the captured context or scheduler. Guidance from the .NET team:
- Library code that doesn't touch UI elements or context-bound state should use it on every await, so it works the same wherever it is called from and avoids the cost of hopping back.
- Application code that needs the context (updating a control after an await) must not use it.
- In ASP.NET Core app code it changes practically nothing; many teams leave it out there.
ConfigureAwait(false) doesn't force a thread switch: if the task has already completed, the method simply continues on the current thread.
ValueTask
Every async Task<T> method that completes asynchronously allocates a Task<T>. When a method usually has its answer ready (a cache hit, data already in a buffer), ValueTask<T> can carry the result directly, with no allocation:
static ValueTask<string> GetCachedAsync(int id) => id == 1 ? ValueTask.FromResult("from cache, no Task allocated") : new ValueTask<string>(LoadAsync(id));It can also wrap a pooled IValueTaskSource, which is how sockets and System.IO.Pipelines read without allocating per operation. The price is a set of rules: await a ValueTask only once, never concurrently, and don't call .Result before it has completed. If you need any of that, call .AsTask() once. Task stays the right default for public APIs; reach for ValueTask where profiling shows the allocations matter.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.