Task Schedulers and the Synchronization Context

On occasion, this chapter has mentioned the task scheduler and its role in determining how to assign work to threads efficiently. Programmatically, the task scheduler is an instance of the System.Threading.Tasks.TaskScheduler. This class, by default, uses the thread pool to schedule tasks appropriately, determining how to safely and efficiently execute them—when to reuse them, dispose them, or create additional ones.

It is possible to create your own task scheduler that makes different choices about how to schedule tasks by deriving a new type from the TaskScheduler class. You can obtain a TaskScheduler that will schedule a task to the current thread (or, more precisely, to the synchronization context associated with the current thread), rather than to a different worker thread, by using the static FromCurrentSynchronizationContext() method.

The synchronization context under which a task executes and, in turn, the continuation task(s) execute(s), is important because the awaiting task consults the synchronization context (assuming there is one) so that a task can execute efficiently and safely. Listing 20.13 (along with Output 20.3) is similar to Listing 19.3, except that it also prints out the thread ID when it displays the message.

Listing 20.13: Calling Task.ContinueWith()
using System;
using System.Threading;
using System.Threading.Tasks;
 
public class Program
{
    public static void Main()
    {
        DisplayStatus("Before");
        Task taskA =
            Task.Run(() =>
                 DisplayStatus("Starting..."))
            .ContinueWith(antecedent =>
                 DisplayStatus("Continuing A..."));
        Task taskB = taskA.ContinueWith(antecedent =>
      DisplayStatus("Continuing B..."));
        Task taskC = taskA.ContinueWith(antecedent =>
            DisplayStatus("Continuing C..."));
        Task.WaitAll(taskB, taskC);
        DisplayStatus("Finished!");
    }
 
    private static void DisplayStatus(string message)
    {
        string text = 
                $@"{ Thread.CurrentThread.ManagedThreadId 
                    }{ message }";
 
 
        Console.WriteLine(text);
    }
}
Output 20.3
1: Before
3: Starting...
4: Continuing A...
3: Continuing C...
4: Continuing B...
1: Finished!

What is noteworthy about this output is that the thread ID changes sometimes and gets repeated at other times. In this kind of plain console application, the synchronization context (accessible from SynchronizationContext.Current) is null—the default synchronization context causes the thread pool to handle thread allocation instead. This explains why the thread ID changes between tasks: Sometimes the thread pool determines that it is more efficient to use a new thread, and sometimes it decides that the best course of action is to reuse an existing thread.

Fortunately, the synchronization context gets set automatically for types of applications where that is critical. For example, if the code creating tasks is running in a thread created by ASP.NET, the thread will have a synchronization context of type AspNetSynchronizationContext associated with it. In contrast, if your code is running in a thread created in a Windows UI application—namely, Windows Presentation Foundation (WPF) or Windows Forms—the thread will have an instance of DispatcherSynchronizationContext or WindowsFormsSynchronizationContext, respectively. (For console applications and Windows Services, the thread will have an instance of the default SynchronizationContext.) Since the TPL consults the synchronization context, and that synchronization context varies depending on the circumstances of the execution, the TPL is able to schedule continuations executing in contexts that are both efficient and safe.

To modify the code so that the synchronization context is leveraged instead, you must (1) set the synchronization context and (2) use async/await to ensure that the synchronization context is consulted.17

It is possible to define custom synchronization contexts and to work with existing synchronization contexts to improve their performance in some specific scenarios. However, describing how to do so is beyond the scope of this text.

________________________________________

17. For a simple example of how to set the synchronization context of a thread and how to use a task scheduler to schedule a task to that thread, see Listing C.8 in Multithreading Patterns Prior to C# 5.0, available at https://intellitect.com/EssentialCSharp.
{{ snackbarMessage }}