📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is a SynchronizationContext, which .NET app models have one, and how does it cause the classic async deadlock?

SynchronizationContext Post and Send, UI and legacy ASP.NET contexts, ASP.NET Core having none, and the blocking-on-async deadlock.

Senior Debugging Advanced async deadlock synchronizationcontext ui-thread

🎯 Key points

  • SynchronizationContext abstracts where code runs, with Post and Send
  • UI frameworks, Blazor and legacy ASP.NET have one; ASP.NET Core and console apps do not
  • await captures the context and posts the continuation back to it
  • Blocking the context's thread on a task that needs it causes a deadlock
  • Fix by awaiting all the way up; use ConfigureAwait(false) in library code
  • After ConfigureAwait(false) do not touch UI controls

⏱ 30-second answer

A SynchronizationContext is an abstraction for "run this code in the right place", with Post (asynchronous) and Send (synchronous). WinForms, WPF, MAUI and Blazor install one so continuations return to the UI thread or renderer; legacy ASP.NET had one per request; ASP.NET Core and console apps have none. await captures the current context and posts the continuation to it. The classic deadlock: UI code calls .Result on an async method, blocking the only UI thread, while the method's continuation waits to be posted back to that same thread. Neither can proceed.

🗣 Full answer (senior)

How I would answer this in an interview

"A SynchronizationContext is an abstraction for 'run this code in the right place'. It has Post (queue it and return) and Send (run it and wait). UI frameworks (WinForms, WPF, MAUI) install one so work returns to the UI thread. Blazor installs one so component updates are serialised. Legacy ASP.NET on .NET Framework had one per request. ASP.NET Core, console apps and worker services have none. When you await an incomplete task, the awaiter captures SynchronizationContext.Current and later posts the continuation back to it. The classic deadlock follows from that. UI code calls .Result on an async method, which blocks the only UI thread. Meanwhile the async method's continuation is waiting to be posted back to that same UI thread. Each waits for the other, forever, with zero CPU. The fix is to await all the way up. Library code uses ConfigureAwait(false) so its continuations don't need the caller's context. In ASP.NET Core this exact deadlock doesn't happen, but blocking still causes ThreadPool starvation, so the advice is the same."


1. What it is

Member Meaning
Post(callback, state) Queue to the target "place" and return immediately (used by await)
Send(callback, state) Run on the target and wait for completion (Control.Invoke style)
SynchronizationContext.Current The context for the current thread (null if none)
OperationStarted/Completed Used by async void to track outstanding operations

2. Which app models have one

App model Context Effect
WinForms WindowsFormsSynchronizationContext Continuations return to the UI thread
WPF DispatcherSynchronizationContext Same, via the Dispatcher
MAUI Platform UI context Same
Blazor Server / WebAssembly Renderer context One logical thread for component state
ASP.NET (.NET Framework) AspNetSynchronizationContext One thread at a time per request; HttpContext.Current
ASP.NET Core, console, worker service None Continuations run on the ThreadPool
Some test frameworks Their own Behaviour may differ from production

3. The deadlock, step by step

Step UI thread Async method
1 Click handler calls LoadAsync().Result Starts, reaches await http.GetAsync(...)
2 — Incomplete → captures UI context, returns incomplete task
3 Blocks in .Result —
4 — HTTP completes → Post continuation to UI context
5 Still blocked: can't run the posted continuation Waits for the UI thread
6 Deadlock: 0% CPU, frozen window

4. Fixes, in order of preference

Fix Effect
await all the way up (async void OK for the event handler only) No blocking; UI stays responsive
ConfigureAwait(false) throughout library code Library continuations don't need the caller's context, so a blocking caller won't deadlock (but is still blocked)
At an unavoidable sync boundary: Task.Run(() => LoadAsync()).GetAwaiter().GetResult() Runs async code without the context; still blocks the caller (last resort)

5. Environment cheat sheet

Environment Blocking on async ConfigureAwait(false)
WPF / WinForms / MAUI Deadlock Yes in libraries; not in code that touches controls
Blazor Deadlock risk As UI
Legacy ASP.NET Deadlock Yes in libraries
ASP.NET Core ThreadPool starvation (no context deadlock) Harmless; optional in app code
Console / worker Starvation risk Optional

Production example

Our [WPF line-of-business app] froze on start-up after a developer added a new async settings loader and called it from the main window's constructor with .Result. The window never appeared, CPU was at [0%], and nothing was logged. A dump of the hung process showed the UI thread in Task.Wait (under .Result), and dumpasync showed the loader's continuation queued to the Dispatcher, waiting for that same thread. I moved the load into an async Loaded event handler that awaited the loader and showed a busy indicator meanwhile, and added ConfigureAwait(false) inside the loader library. We also added the Microsoft.VisualStudio.Threading.Analyzers package so synchronous waits on the UI thread are flagged at build time.

🔍 What the interviewer is checking

Checks a classic senior async debugging topic.

Green flags

  • Explains what a context is and which app models have one.
  • Walks through the deadlock step by step.
  • Knows ASP.NET Core has no context.
  • Fixes by awaiting all the way, with ConfigureAwait(false) in libraries.

Red flags

  • "Add ConfigureAwait(false) everywhere" without knowing why.
  • Thinks ASP.NET Core deadlocks the same way.

📚 Learn in depth

1. How await captures and resumes

await task (incomplete):
    ctx = SynchronizationContext.Current
    if (ctx != null && ctx.GetType() != typeof(SynchronizationContext))
        register continuation: on completion -> ctx.Post(MoveNext)
    else if (TaskScheduler.Current != TaskScheduler.Default)
        register continuation on that scheduler
    else
        register continuation to run on the pool (or inline on the completing thread when safe)

ConfigureAwait(false) skips the first two branches.

2. Reproducing the deadlock without a UI

The code example builds a tiny single-threaded SynchronizationContext (a queue processed by one thread), just like a UI message loop:

fake-ui thread runs callback:
    LoadAsync() -> await Task.Delay(100) captures fake-ui context
    .GetResult() blocks fake-ui thread
Delay completes -> Post(continuation) to fake-ui queue
fake-ui thread is blocked -> queue never processed -> deadlock

With avoidContext: true (ConfigureAwait(false)), the continuation runs on the pool, completes the task, and .GetResult() returns.

3. Send deadlocks too

// UI thread:
var t = Task.Run(() => uiContext.Send(_ => label.Text = "x", null));   // background thread waits for UI
t.Wait();                                                               // UI thread waits for background -> deadlock

Prefer Post / BeginInvoke / Dispatcher.InvokeAsync, or just await.

4. Why ASP.NET Core dropped it

  • AspNetSynchronizationContext serialised continuations per request and carried HttpContext.Current, costing throughput and causing the deadlock class above.
  • ASP.NET Core passes HttpContext explicitly (DI, parameters) or via IHttpContextAccessor (AsyncLocal).
  • Without a context, continuations run on any pool thread; code must not assume thread affinity.

5. async void and the context

async void methods call OperationStarted / OperationCompleted on the context and post exceptions to it. On a UI thread, an exception from an async void handler goes to the dispatcher's unhandled-exception handler; with no context, it crashes the process.

6. Diagnosing in a dump

Clue Meaning
UI thread top frames: Task.Wait, TaskAwaiter.GetResult, .Result Blocking on async
dumpasync shows a state machine whose continuation targets the dispatcher/context Waiting for the blocked thread
CPU ~0%, no exceptions Classic deadlock signature

7. Test frameworks

xUnit v2 installs a context (to limit parallelism); some UI test harnesses install dispatcher contexts. Code that blocks on async may deadlock in one test runner and not another. Make tests async Task and await.

8. Version notes

Version Change
.NET Framework 2.0 SynchronizationContext
.NET 4.5 / C# 5 await captures context by default
ASP.NET Core 1.0 No request context
.NET 8 ConfigureAwaitOptions (see next question)
.NET 10 Same model

9. Try it yourself

  1. Run the code example with avoidContext: false to see "Deadlocked", then true to see "Completed".
  2. In WPF, call .Result on an async method from a button click and observe the freeze; then make the handler async void and await.
  3. Add ConfigureAwait(false) in the async method and confirm .Result no longer deadlocks (but still blocks the UI).
  4. Print SynchronizationContext.Current?.GetType().Name in WPF, Blazor, ASP.NET Core and a console app.
  5. Take a dump of the frozen WPF app and inspect the UI thread with clrstack and dumpasync.

💻 Code

using System.Threading;

// Simulate a UI-style single-threaded context to reproduce the deadlock in a console app.
var context = new SingleThreadContext();
var done = new ManualResetEventSlim();

context.Post(_ =>
{
    SynchronizationContext.SetSynchronizationContext(context);
    var task = LoadAsync(avoidContext: true); // set false to deadlock
    Console.WriteLine($"Result: {task.GetAwaiter().GetResult()}"); // blocks the context thread
    done.Set();
}, null);

Console.WriteLine(done.Wait(TimeSpan.FromSeconds(2)) ? "Completed" : "Deadlocked");

static async Task<string> LoadAsync(bool avoidContext)
{
    // continueOnCapturedContext: false is the same as ConfigureAwait(false).
    await Task.Delay(100).ConfigureAwait(continueOnCapturedContext: !avoidContext);
    return "settings";
}

// A minimal single-threaded SynchronizationContext, like a UI thread's.
public sealed class SingleThreadContext : SynchronizationContext
{
    private readonly System.Collections.Concurrent.BlockingCollection<(SendOrPostCallback, object?)> _queue = new();

    public SingleThreadContext()
    {
        var thread = new Thread(() =>
        {
            foreach (var (callback, state) in _queue.GetConsumingEnumerable()) callback(state);
        }) { IsBackground = true, Name = "fake-ui" };
        thread.Start();
    }

    public override void Post(SendOrPostCallback d, object? state) => _queue.Add((d, state));
}

⚠ Common mistakes

1. Blocking on async code from a UI thread

What happens: .Result / .Wait() / GetAwaiter().GetResult() in a handler or constructor. Why it is wrong: the continuation needs the blocked thread → deadlock. Do instead: make the caller async and await; move work out of constructors.

2. Assuming ASP.NET Core has a synchronisation context

What happens: HttpContext.Current-style code or deadlock-driven ConfigureAwait rules are applied. Why it is wrong: there's no context; continuations run on any pool thread. Do instead: pass HttpContext explicitly; still avoid blocking because it starves the pool.

3. Touching UI after ConfigureAwait(false)

What happens: label.Text = ... after an awaited call configured with false. Why it is wrong: the code is on a pool thread → cross-thread exception. Do instead: use ConfigureAwait(false) only in code that doesn't touch the UI.

4. Using Send / Invoke from a thread the UI is waiting on

What happens: background work calls Control.Invoke while the UI thread waits for that work. Why it is wrong: mutual wait → deadlock. Do instead: BeginInvoke/InvokeAsync, or await.

5. "Fixing" deadlocks with ConfigureAwait(false) while still blocking

What happens: the deadlock disappears but .Result stays on the UI thread. Why it is wrong: the UI still freezes for the duration of the call. Do instead: remove the blocking call.

6. Starting async work in constructors

What happens: constructors call async methods and block on them. Why it is wrong: constructors can't be async; blocking risks deadlock. Do instead: async factory methods, or load in an async lifecycle event (Loaded, OnInitializedAsync).

7. async void for non-handler methods in UI apps

What happens: helper methods are async void. Why it is wrong: callers can't await them; exceptions go to the dispatcher or crash. Do instead: async Task.

8. Assuming tests reflect production context

What happens: code deadlocks in xUnit but not in the app (or vice versa). Why it is wrong: different contexts change behaviour. Do instead: write async tests and never block in tested code.

9. Using Task.Run(...).Result as the default fix

What happens: every sync-over-async call is wrapped this way. Why it is wrong: avoids the deadlock but still blocks a thread and adds a hop. Do instead: keep it for rare, unavoidable sync boundaries.

10. Forgetting Blazor has a context

What happens: Blazor components block on async calls or mutate state from background threads. Why it is wrong: deadlocks or renderer exceptions. Do instead: await, and use InvokeAsync(StateHasChanged) from background callbacks.

🏛 Senior insight: trade-offs

Knowing which app model you are in decides the right advice. In UI apps the context is essential and blocking deadlocks; in ASP.NET Core there is no context but blocking still causes starvation. Libraries must work in both, which is why they use ConfigureAwait(false).

Environment Blocking on async ConfigureAwait(false) needed?
WPF/WinForms/MAUI Deadlock Yes in libraries; no in UI code that touches controls
Blazor Deadlock risk Same as UI
Legacy ASP.NET Deadlock Yes in libraries
ASP.NET Core Starvation, not this deadlock Harmless, optional in app code
Console/worker Starvation risk Optional

🔗 Follow-ups the interviewer may ask

  1. When: Candidate suggests ConfigureAwait(false) in libraries Why: Checks when ConfigureAwait applies
    How to answer

    ConfigureAwait(false) tells an await not to resume on the captured SynchronizationContext or custom TaskScheduler; the continuation runs on a ThreadPool thread instead. Use it in general-purpose library code, which should not depend on or block the caller's context, to avoid deadlocks and save context switches. Do not use it in UI code that must touch controls after the await. In ASP.NET Core application code it is optional because there is no context. Since .NET 8, ConfigureAwait(ConfigureAwaitOptions) adds SuppressThrowing (await a Task without throwing) and ForceYielding (always continue asynchronously).

  2. When: Candidate says await captures the context Why: Checks await mechanics
    How to answer

    async tells the compiler to rewrite the method into a state machine and lets you use await inside it; it does not create a thread. At await x, the code calls x.GetAwaiter() and checks IsCompleted. If it is already complete, execution continues synchronously. If not, the method registers the rest of itself as a continuation, captures the current SynchronizationContext (or non-default TaskScheduler), and returns an incomplete task to its caller, releasing the thread. When the awaited work finishes, the continuation resumes, on the captured context if there was one, otherwise on the ThreadPool. Exceptions and results flow through the returned task.

🛠 Real-world problems: how you handle them

WPF start-up freeze from .Result on an async loader
Situation
A [WPF line-of-business app] added an async settings loader called from a window constructor with `.Result`.
Signals
The app froze on start-up with [0%] CPU; no exception.
Investigation
A dump of the hung process showed the UI thread in `Task.Wait` and the loader's continuation queued to the dispatcher.
Resolution
Moved loading to an `async` `Loaded` event handler that awaited the loader, and showed a busy indicator.
Prevention
Added the VSTHRD analysers (Microsoft.VisualStudio.Threading.Analyzers) to flag synchronous waits on the UI thread.

How to tell it (STAR)

Our [WPF app] froze at start-up with no CPU. The dump showed the UI thread blocked on `.Result` and the continuation waiting for that same thread. I moved the load into an async `Loaded` handler and added threading analysers so the pattern is flagged at build time.

↩ Usually asked after

More in Async Advanced

Last updated 31 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me