Skip to content

Instantly share code, notes, and snippets.

@SaifAqqad
Last active March 6, 2024 08:53
Show Gist options
  • Save SaifAqqad/b9004dbfcf503ebbf8e24e59e7727506 to your computer and use it in GitHub Desktop.
Save SaifAqqad/b9004dbfcf503ebbf8e24e59e7727506 to your computer and use it in GitHub Desktop.

The difference between ExecutionContext and SynchronizationContext

ExecutionContext is a state bag that captures and stores ambient state (near-by variables, other data) from the current thread and offers a way to run delegates with a specific stored ExecutionContext as the ambient state, even if we're on a different thread.

When using ExecutionContext, we're capturing the ambient state from the invoking thread and then restoring that state on the other thread when it's invoking a task completion or some other delegate


SynchronizationContext is just an abstraction over a method of invoking delegates that's specific to a given environment (like Control.BeginInvoke() for WPF or Dispatcher.BeginInvoke() for winforms)

ex: Both WPF and windows forms offer a custom SynchronizationContext that invokes delegates on the UI Thread, so instead of using the API thats specific to that environment, we use the abstraction (SynchronizationContext) to make our components framework-agnostic

When using SynchronizationContext, we're capturing the -method of invoking delegates- from the current thread and using that same method to invoke the task's completion or other delegates

When do we need task.ConfigureAwait(false)?

ConfigureAwait is needed for cases where the environment that's used to run code is not known when writing it (ex: libraries) and so they need to handle cases where the current synchronization context would limit concurrent execution (like in the case of wpf and its UI Thread) to avoid deadlocks that would happen if the async method was not awaited and instead was synchronously (and blockingly) waiting for the result

Deadlock case: (WPF example)

    var task = DoAsync();
    control.Text = task.Result;
  • UI Thread: calls async method DoAsync() and executes the synchrounous part (before await) and stores the current ExecutionContext and SynchronizationContext in the returned task
  • UI Thread: Blocks to get the task's result (which isn't available yet)
  • ThreadPool Thread: Executes the async part of DoAsync()
  • ThreadPool Thread: Enqueues the continuation (alongside the results) of the DoAsync() call using the SynchronizationContext that was stored
  • The stored SynchronizationContext can only run delegates on 1 thread which is currently blocking to get the result
  • DEADLOCK !!!

async/await automatically interacts with both ExecutionContext and SynchronizationContext, so the ambient state flows on it's own from the code before the await and after it (which is called the task's continuation) and the SynchronizationContext that was used to call an async method will automactically be used to call its continuation (the code after the await)

Whenever code awaits an awaitable whose awaiter says it’s not yet complete (i.e. the awaiter’s IsCompleted returns false), the method needs to suspend, and it’ll resume via a continuation off of the awaiter, and thus, ExecutionContext needs to flow from the code issuing the await through to the continuation delegate’s execution. That’s handled automatically by the Runtime. When the async method is about to suspend, the infrastructure captures an ExecutionContext. The delegate that gets passed to the awaiter has a reference to this ExecutionContext instance and will use it when resuming the method. This is what enables the important “ambient” information represented by ExecutionContext to flow across awaits.

When you await a task, by default the awaiter will capture the current SynchronizationContext, and if there was one, when the task completes it’ll Post the supplied continuation delegate back to that context, rather than running the delegate on whatever thread the task completed on or rather than scheduling it to run on the ThreadPool. If a developer doesn’t want this marshaling behavior, it can be controlled by changing the awaitable/awaiter that’s used. Whereas this behavior is always employed when you await a Task or Task<TResult>, you can instead await the result of calling task.ConfigureAwait(…). The ConfigureAwait method returns an awaitable that enables this default marshaling behavior to be suppressed. Whether it’s suppressed is controlled by a Boolean passed to the ConfigureAwait method. If continueOnCapturedContext is true, then you get the default behavior; if it’s false, the awaiter doesn’t check for a SynchronizationContext, pretending as if there wasn’t one. (Note that when the awaited task completes, regardless of ConfigureAwait, the runtime may check the context that’s current on the resuming thread to determine whether it’s ok to synchronously run the continuation there or whether the continuation must be scheduled asynchronously from that point.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment