Asynchrony and Threading - Tigra-Astronomy/TA.Utilities GitHub Wiki

Asynchrony and Threading

A Word about ConfigureAwait()

There is an extension method in .NET used to configure awaitable tasks, called ConfigureAwait(bool). THe method affects how the task awaiter schedules its continuation. With ConfigureAwait(true) the tasks continues on the current synchronization context. That usually means on the same thread, and is particularly relevant when the awaiter is a user interface thread. Conversely, ConfigureAwait(false) means that continuation can happen on any thread, and usually that will be a thread pool worker thread. The implications are quite profound. Consider the following method:

public async Task SomeMethod()
    {
	Console.WriteLine("Starting on thread {0}", Thread.CurrentThread.ManagedThreadId);
	await Task.Delay(1000).ConfigureAwait(false);
	Console.WriteLine("Continuing on thread {0}", Thread.CurrentThread.ManagedThreadId);
    }

When you run this, you may get something like

Starting on thread 14
Continuing on thread 11

But it is not at all ovious how ConfigureAwait() should be used. What if you don't specifiy? Is the await configured or unconfigured? Does ConfigureAwait(false) mean you don't want to configure it, or that you want to configure it not to do something? It's just horrible, you can't read the code and instantly understand what it does, and that violates the Principle of Least Astonishment.

So we made some extension methods that essentially do the same thing, but make more sense.

task.ContinueOnAnyThread()

Our aync method now becomes:

public async Task SomeMethod()
    {
	Console.WriteLine("Starting on thread {0}", Thread.CurrentThread.ManagedThreadId);
	await Task.Delay(1000).ContinueOnAnyThread();
	Console.WriteLine("Continuing on thread {0}", Thread.CurrentThread.ManagedThreadId);
    }

and we get

Starting on thread 15
Continuing on thread 13

Alternatively:

task.ContinueInCurrentContext()

public async Task SomeMethod()
    {
	Console.WriteLine("Starting on thread {0}", Thread.CurrentThread.ManagedThreadId);
	await Task.Delay(1000).ContinueInCurrentContext();
	Console.WriteLine("Continuing on thread {0}", Thread.CurrentThread.ManagedThreadId);
    }

The await captures the current SynchronizationContext and uses it to schedule the continuation. What happens next depends on the application model and how it implements SynchronizationContext. For a user interface application, the UI generally runs in a Single Threaded Apartment (STA thread). In this model, asynchronous operations are posted to the message queue of the STA thread. The continuation will then happen on the UI thread once the thread is idel and the message pump runs. In a free-threaded application model such as a console application, the continuation will likely still happen on a different thread.

Asynchronous ≠ Multi-threaded

Here you can see the danger of this option. If a task continuation is posted to the message queue of an STA thread, waiting for messages to be pumped, but the UI is blocked waiting for the task to complete, then the continuation may never get to run. The task is prevented from completing and we are in deadlock. Therefore, as a library writer, you need to help your users not fall into this trap. The best practice for library writers is to always use ContinueOnAnyThread() so that task continuations can always execute on a thread pool thread regardless of the state of the UI thread.

Cancel Culture

One final extension method is Task.WithCancellation(token). This takes a task that is not cancellable and adds cancellation to it. Note that task cancellation is cooperative. There's no way to preempt a task that was not created with cancellation in mind. So adding cancellation to such a task doesn't stop the task from running and it may still run to completion. It just means that you can stop waiting for it to complete.