The UI thread and SynchronizationContext

1 min read

WPF, WinForms and MAUI run on one dedicated UI thread with a message loop. It is the same shape as Node’s event loopNode's event loop compared with the .NET thread poolNode runs your code on one thread. .NET runs it on many.Opens in a new tab, with input, paint and timers in the queue.

text
while (true) {    message = GetNextMessage(); // click, keypress, paint, timer, posted work...    Dispatch(message);          // run its handler, to completion}

One thread only

UI controls are not thread-safe by design, for performance. Touching one from another thread throws “The calling thread cannot access this object because a different thread owns it”. A background thread posts the call to the UI thread’s queue: Dispatcher.Invoke in WPF, this.Invoke in WinForms.

SynchronizationContext

await captures the current SynchronizationContext and resumes on it, which in a UI app means posting back to the UI thread. The code after the await can touch controls with no Invoke. ConfigureAwait(false) opts out, and libraries use it. The handler is async void because event delegates return void (see async voidWhy async void is dangerousIt discards the Task, so the caller cannot await it or catch its exceptions.Opens in a new tab).

C#
async void Button_Click(object sender, EventArgs e){    label.Text = "loading...";    var data = await httpClient.GetStringAsync(url); // the thread is freed during the wait    label.Text = data;                               // this just works, and touches the UI safely}

Where it matters

Browser JavaScript, Node and Android have the same shape: one thread owns a resource, a queue feeds it, and other threads post messages instead of touching it. See how await worksHow await works under the hoodThe compiler turns an async method into a state machine, and the thread is freed while it waits.Opens in a new tab.