Parallelism in .NET – Part 15, Making Tasks Run: The TaskScheduler
Posted
by Reed
on Reed Copsey
See other posts from Reed Copsey
or by Reed
Published on Fri, 19 Mar 2010 01:10:46 +0000
Indexed on
2010/12/06
17:00 UTC
Read the original article
Hit count: 1188
In my introduction to the Task class, I specifically made mention that the Task class does not directly provide it’s own execution. In addition, I made a strong point that the Task class itself is not directly related to threads or multithreading. Rather, the Task class is used to implement our decomposition of tasks.
Once we’ve implemented our tasks, we need to execute them. In the Task Parallel Library, the execution of Tasks is handled via an instance of the TaskScheduler class.
The TaskScheduler class is an abstract class which provides a single function: it schedules the tasks and executes them within an appropriate context. This class is the class which actually runs individual Task instances. The .NET Framework provides two (internal) implementations of the TaskScheduler class.
Since a Task, based on our decomposition, should be a self-contained piece of code, parallel execution makes sense when executing tasks. The default implementation of the TaskScheduler class, and the one most often used, is based on the ThreadPool. This can be retrieved via the TaskScheduler.Default property, and is, by default, what is used when we just start a Task instance with Task.Start().
Normally, when a Task is started by the default TaskScheduler, the task will be treated as a single work item, and run on a ThreadPool thread. This pools tasks, and provides Task instances all of the advantages of the ThreadPool, including thread pooling for reduced resource usage, and an upper cap on the number of work items. In addition, .NET 4 brings us a much improved thread pool, providing work stealing and reduced locking within the thread pool queues. By using the default TaskScheduler, our Tasks are run asynchronously on the ThreadPool.
There is one notable exception to my above statements when using the default TaskScheduler. If a Task is created with the TaskCreationOptions set to TaskCreationOptions.LongRunning, the default TaskScheduler will generate a new thread for that Task, at least in the current implementation. This is useful for Tasks which will persist for most of the lifetime of your application, since it prevents your Task from starving the ThreadPool of one of it’s work threads.
The Task Parallel Library provides one other implementation of the TaskScheduler class. In addition to providing a way to schedule tasks on the ThreadPool, the framework allows you to create a TaskScheduler which works within a specified SynchronizationContext. This scheduler can be retrieved within a thread that provides a valid SynchronizationContext by calling the TaskScheduler.FromCurrentSynchronizationContext() method.
This implementation of TaskScheduler is intended for use with user interface development. Windows Forms and Windows Presentation Foundation both require any access to user interface controls to occur on the same thread that created the control. For example, if you want to set the text within a Windows Forms TextBox, and you’re working on a background thread, that UI call must be marshaled back onto the UI thread. The most common way this is handled depends on the framework being used. In Windows Forms, Control.Invoke or Control.BeginInvoke is most often used. In WPF, the equivelent calls are Dispatcher.Invoke or Dispatcher.BeginInvoke.
As an example, say we’re working on a background thread, and we want to update a TextBlock in our user interface with a status label. The code would typically look something like:
// Within background thread work... string status = GetUpdatedStatus(); Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action( () => { statusLabel.Text = status; })); // Continue on in background method
This works fine, but forces your method to take a dependency on WPF or Windows Forms. There is an alternative option, however. Both Windows Forms and WPF, when initialized, setup a SynchronizationContext in their thread, which is available on the UI thread via the SynchronizationContext.Current property. This context is used by classes such as BackgroundWorker to marshal calls back onto the UI thread in a framework-agnostic manner.
The Task Parallel Library provides the same functionality via the TaskScheduler.FromCurrentSynchronizationContext() method. When setting up our Tasks, as long as we’re working on the UI thread, we can construct a TaskScheduler via:
TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
We then can use this scheduler on any thread to marshal data back onto the UI thread. For example, our code above can then be rewritten as:
string status = GetUpdatedStatus(); (new Task(() => { statusLabel.Text = status; })) .Start(uiScheduler); // Continue on in background method
This is nice since it allows us to write code that isn’t tied to Windows Forms or WPF, but is still fully functional with those technologies. I’ll discuss even more uses for the SynchronizationContext based TaskScheduler when I demonstrate task continuations, but even without continuations, this is a very useful construct.
In addition to the two implementations provided by the Task Parallel Library, it is possible to implement your own TaskScheduler. The ParallelExtensionsExtras project within the Samples for Parallel Programming provides nine sample TaskScheduler implementations. These include schedulers which restrict the maximum number of concurrent tasks, run tasks on a single threaded apartment thread, use a new thread per task, and more.
© Reed Copsey or respective owner