Scheduling - Concurrency in C# Cookbook (2014)

Concurrency in C# Cookbook (2014)

Chapter 12. Scheduling

When a piece of code executes, it has to run on some thread somewhere. A scheduler is an object that decides where a certain piece of code runs. There are a few different scheduler types in the .NET framework, and they’re used with slight differences by parallel and dataflow code.

I recommend that you not specify a scheduler whenever possible; the defaults are usually correct. For example, the await operator in asynchronous code will automatically resume the method within the same context, unless you override this default, as described in Recipe 2.7. Similarly, reactive code has reasonable default contexts for raising its events, which you can override with ObserveOn, as described in Recipe 5.2

However, if you need other code to execute in a specific context (e.g., a UI thread context, or an ASP.NET request context), then you can use the scheduling recipes in this chapter to control the scheduling of your code.

12.1. Scheduling Work to the Thread Pool

Problem

You have a piece of code that you explicitly want to execute on a thread-pool thread.

Solution

The vast majority of the time, you’ll want to use Task.Run, which is quite simple. The following code blocks a thread-pool thread for two seconds:

Task task = Task.Run(() =>

{

Thread.Sleep(TimeSpan.FromSeconds(2));

});

Task.Run also understands return values and asynchronous lambdas just fine. The task returned by Task.Run in the following code will complete after two seconds with a result of 13:

Task<int> task = Task.Run(async () =>

{

await Task.Delay(TimeSpan.FromSeconds(2));

return 13;

});

Task.Run returns a Task (or Task<T>), which can be naturally consumed by asynchronous or reactive code.

Discussion

Task.Run is ideal for UI applications, when you have time-consuming work to do that cannot be done on the UI thread. For example, Recipe 7.4 uses Task.Run to push parallel processing to a thread-pool thread. However, do not use Task.Run on ASP.NET unless you are absolutely sure you know what you’re doing. On ASP.NET, request handling code is already running on a thread-pool thread, so pushing it onto another thread-pool thread is usually counterproductive.

Task.Run is an effective replacement for BackgroundWorker, Delegate.BeginInvoke, and ThreadPool.QueueUserWorkItem. None of those should be used in new code; code using Task.Run is much easier to write correctly and maintain over time. Furthermore, Task.Runhandles the vast majority of use cases for Thread, so most uses of Thread can also be replaced with Task.Run (with rare exceptions, such as Single-Thread Apartment threads).

Parallel and dataflow code executes on the thread pool by default, so there’s usually no need to use Task.Run with code executed by the Parallel, Parallel LINQ, or TPL Dataflow libraries.

If you are doing dynamic parallelism, then use Task.Factory.StartNew instead of Task.Run. This is necessary because the Task returned by Task.Run has its default options configured for asynchronous use (i.e., to be consumed by asynchronous or reactive code). It does not support advanced concepts, such as parent/child tasks, which are more common in dynamic parallel code.

See Also

Recipe 7.6 covers consuming asynchronous code (such as the task returned from Task.Run) with reactive code.

Recipe 7.4 covers asynchronously waiting for parallel code, which is most easily done via Task.Run.

Recipe 3.4 covers dynamic parallelism, a scenario where you should use Task.Factory.StartNew instead of Task.Run.

12.2. Executing Code with a Task Scheduler

Problem

You have multiple pieces of code that you need to execute in a certain way. For example, you may need all the pieces of code to execute on the UI thread, or you may need to execute only a certain number at a time.

This recipe deals with how to define and construct a scheduler for those pieces of code. Actually applying that scheduler is the subject of the next two recipes.

Solution

There are quite a few different types in .NET that can handle scheduling; this recipe focuses on TaskScheduler because it is portable and relatively easy to use.

The simplest TaskScheduler is TaskScheduler.Default, which queues work to the thread pool. You will seldomly specify TaskScheduler.Default in your own code, but it is important to be aware of it, since it is the default for many scheduling scenarios. Task.Run, parallel, and dataflow code all use TaskScheduler.Default.

You can capture a specific context and later schedule work back to it by using TaskScheduler.FromCurrentSynchronizationContext, as follows:

TaskScheduler scheduler = TaskScheduler.FromCurrentSynchronizationContext();

This creates a TaskScheduler that captures the current SynchronizationContext and schedules code onto that context. SynchronizationContext is a type that represents a general-purpose scheduling context. There are several different contexts in the .NET framework; most UI frameworks provide a SynchronizationContext that represents the UI thread, and ASP.NET provides a SynchronziationContext that represents the HTTP request context.

Another powerful type introduced in .NET 4.5 is the ConcurrentExclusiveSchedulerPair, which is actually two schedulers that are related to each other. The ConcurrentScheduler member is a scheduler that allows multiple tasks to execute at the same time, as long as no task is executing on the ExclusiveScheduler. The ExclusiveScheduler only executes code one task at a time, and only when there is no task already executing on the ConcurrentScheduler:

var schedulerPair = new ConcurrentExclusiveSchedulerPair();

TaskScheduler concurrent = schedulerPair.ConcurrentScheduler;

TaskScheduler exclusive = schedulerPair.ExclusiveScheduler;

One common use for ConcurrentExclusiveSchedulerPair is to just use the ExclusiveScheduler to ensure only one task is executed at a time. Code that executes on the ExclusiveScheduler will run on the thread pool but will be restricted to executing exclusive of all other code using the same ExclusiveScheduler instance.

Another use for ConcurrentExclusiveSchedulerPair is as a throttling scheduler. You can create a ConcurrentExclusiveSchedulerPair that will limit its own concurrency. In this scenario, the ExclusiveScheduler is usually not used:

var schedulerPair = new ConcurrentExclusiveSchedulerPair(TaskScheduler.Default,

maxConcurrencyLevel: 8);

TaskScheduler scheduler = schedulerPair.ConcurrentScheduler;

Note that this kind of throttling only throttles code while it is executing; it is quite different than the kind of logical throttling covered in Recipe 11.5. In particular, asynchronous code is not considered to be executing while it is awaiting an operation. The ConcurrentScheduler throttles executing code; other throttling, such as SemaphoreSlim, throttles at a higher level (i.e., an entire async method).

Discussion

You may have noticed that the last code example passed TaskScheduler.Default into the constructor for ConcurrentExclusiveSchedulerPair. This is because ConcurrentExclusiveSchedulerPair actually applies its concurrent/exclusive logic around an existingTaskScheduler.

This recipe introduces TaskScheduler.FromCurrentSynchronizationContext, which is useful to execute code on a captured context. It is also possible to use SynchoronizationContext directly to execute code on that context; however, I do not recommend this approach. Whenever possible, use the await operator to resume on an implicitly captured context or use a TaskScheduler wrapper.

Do not ever use platform-specific types to execute code on a UI thread. WPF, Silverlight, iOS, and Android all provide the Dispatcher type, Windows Store uses the CoreDispatcher, and Windows Forms has the ISynchronizeInvoke interface (i.e., Control.Invoke). Do not use any of these types in new code; just pretend they don’t exist. Using them will tie your code to a specific platform unnecessarily. SynchronizationContext is a general-purpose abstraction around these types.

Reactive Extensions introduces a more general scheduler abstraction: IScheduler. An Rx scheduler is capable of wrapping any other kind of scheduler; the TaskPoolScheduler will wrap any TaskFactory (which contains a TaskScheduler). The Rx team also defined anIScheduler implementation that can be manually controlled for testing. If you need to actually use a scheduler abstraction, I’d recommend using the IScheduler from Rx; it’s well designed, well defined, and test friendly. However, most of the time you don’t need a scheduler abstraction, and earlier libraries, such as the Task Parallel Library and TPL Dataflow, only understand the TaskScheduler type.

See Also

Recipe 12.3 covers applying a TaskScheduler to parallel code.

Recipe 12.4 covers applying a TaskScheduler to dataflow code.

Recipe 11.5 covers higher-level logical throttling.

Recipe 5.2 covers Reactive Extensions schedulers for event streams.

Recipe 6.6 covers the Reactive Extensions test scheduler.

12.3. Scheduling Parallel Code

Problem

You need to control how the individual pieces of code are executed in parallel code.

Solution

Once you create an appropriate TaskScheduler instance (see Recipe 12.2), you can include it in the options that you pass to a Parallel method. The following code takes a sequence of sequences of matrices; it starts a bunch of parallel loops and wants to limit the total parallelism of all loops simultaneously, regardless of how many matrices are in each sequence:

void RotateMatrices(IEnumerable<IEnumerable<Matrix>> collections, float degrees)

{

var schedulerPair = new ConcurrentExclusiveSchedulerPair(

TaskScheduler.Default, maxConcurrencyLevel: 8);

TaskScheduler scheduler = schedulerPair.ConcurrentScheduler;

ParallelOptions options = new ParallelOptions { TaskScheduler = scheduler };

Parallel.ForEach(collections, options,

matrices => Parallel.ForEach(matrices, options,

matrix => matrix.Rotate(degrees)));

}

Discussion

Parallel.Invoke also takes an instance of ParallelOptions, so you can pass a TaskScheduler to Parallel.Invoke the same way as Parallel.ForEach. If you are doing dynamic parallel code, you can pass TaskScheduler directly to TaskFactory.StartNew orTask.ContinueWith.

There is no way to pass a TaskScheduler to Parallel LINQ (PLINQ) code.

See Also

Recipe 12.2 covers common task schedulers and how to choose between them.

12.4. Dataflow Synchronization Using Schedulers

Problem

You need to control how the individual pieces of code are executed in dataflow code.

Solution

Once you create an appropriate TaskScheduler instance (see Recipe 12.2), you can include it in the options that you pass to a dataflow block. When called from the UI thread, the following code creates a dataflow mesh that multiples all of its input values by two (using the thread pool) and then appends the resulting values to the items of a list box (on the UI thread):

var options = new ExecutionDataflowBlockOptions

{

TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(),

};

var multiplyBlock = new TransformBlock<int, int>(item => item * 2);

var displayBlock = new ActionBlock<int>(

result => ListBox.Items.Add(result), options);

multiplyBlock.LinkTo(displayBlock);

Discussion

Specifying a TaskScheduler is especially useful if you need to coordinate the actions of blocks in different parts of your dataflow mesh. For example, you can use the ConcurrentExclusiveSchedulerPair.ExclusiveScheduler to ensure that blocks A and C never execute code at the same time, while allowing block B to execute whenever it wants.

Keep in mind that synchronization by TaskScheduler only applies while the code is executing. For example, if you have an action block that runs asynchronous code and you apply an exclusive scheduler, the code is not considered running when it is awaiting.

You can specify a TaskScheduler for any kind of dataflow block. Even though a block may not execute your code (e.g., BufferBlock<T>), it still has housekeeping tasks that it needs to do, and it will use the provided TaskScheduler for all of its internal work.

See Also

Recipe 12.2 covers common task schedulers and how to choose between them.