SynchronizationContext is a .NET abstraction for scheduling callbacks onto a particular execution context, such as a UI thread. Background work can use it to report results without directly touching UI objects from the wrong thread. section 4.3 section 11.2
A context is associated with a thread through SynchronizationContext.SetSynchronizationContext. Code running on that thread can then capture the current context, allowing await to schedule continuation code appropriately. section 11.2 section 5.2
Use the context according to the callback behavior required:
- Post queues a callback and returns without waiting; await uses Post for continuations. section 11.2
- Send runs a callback on the target context and waits for it to finish. section 11.2
- WinForms and WPF provide framework-specific alternatives through Control.BeginInvoke or Dispatcher.BeginInvoke, while SynchronizationContext avoids direct UI-framework dependencies. section 4.3 section 11.2
A custom context can maintain a callback queue and have one thread consume it. Posting adds work to the queue; the consuming loop executes each callback on its own thread. This models how a single-threaded environment can preserve execution affinity. section 11.2
Preserving a single execution thread
A SingleThreadSyncContext can install itself, run an asynchronous startup delegate, and process queued continuations until the operation completes. With the context installed, code after await Task.Delay can run on the same managed thread rather than an arbitrary thread-pool thread. section 11.2
public static void Run(Func<Task> startup)
{
var prev = SynchronizationContext.Current;
try
{
var ctxt = new SingleThreadSyncContext();
SynchronizationContext.SetSynchronizationContext(ctxt); #1
ctxt.Loop(startup); #2
}
finally
{
SynchronizationContext.SetSynchronizationContext(prev); #3
}
}
Shows installing a custom context, running the event loop, and restoring the previous context. section 11.2
private BlockingCollection<(SendOrPostCallback call,object? state)>
_queue = new();
private void Loop(Func<Task> startup)
{
startup().ContinueWith(t => _queue.CompleteAdding()); #1
foreach(var next in _queue.GetConsumingEnumerable()) #2
{ #2
next.call(next.state); #2
} #2
}
Shows the blocking collection and consuming loop that execute posted callbacks on the context thread. section 11.2
Blocking the UI thread while waiting for work that must return through Send, Post, or await can deadlock: the callback cannot run until the blocked thread becomes available, leaving the UI frozen. section 4.3 section 5.5
