Parallel Tasks (Multithreading) |
The DigitalRune Base library supports running multiple tasks in parallel. This section explains how to utilize multithreading in your application.
This topic contains the following sections:
The namespace DigitalRune.Threading and the class Parallel provides support for concurrency to run multiple task in parallel and automatically balance work across all available processors. The implementation is a replacement for Microsoft's Task Parallel Library which is not yet supported by the .NET Compact Framework. The class Parallel provides a lightweight and cross-platform implementation. The library supports Windows, Silverlight, Windows Phone 7, and Xbox 360).
The API has similarities to Microsoft's Task Parallel Library, but it is not identical. The names in the namespace DigitalRune.Threading conflict with the types of the namespace System.Threading.Tasks. This is on purpose as only one solution for concurrency should be used in an application. The library has been optimized for the .NET Compact Framework: Only the absolute minimum of memory is allocated at runtime.
The DigitalRune libraries make extensive use of the class Parallel. We highly recommend, that if you need support for multithreading in your application, you should take advantage of this class. (Using different solutions for concurrency can reduce performance.)
A task is an asynchronous operation which is started, for example, by calling ParallelStart(Action). The method returns a handle of type Task. This handle can be used to query the status of the asynchronous operation (see property IsComplete). The method Wait can be called to wait until the operation has completed.
// Start a method call on another thread: // DoSomeWork can either be an Action delegate, or an object which implements IWork. Task task = Parallel.Start(DoSomeWork); // Do something else on this thread for a while. DoSomethingElse(); // Wait for the task to complete. This ensures that after this call returns, the // task has finished. task.Wait();
A future is an asynchronous operation that returns a value. A future is created, for example, by calling ParallelStartT(FuncT) and specifying a function that computes a value. The method returns a handle of type TaskT, which is similar to Task. The result of a future can be queried by calling GetResult. Note that GetResult can only be called once - the handle becomes invalid after the first call!
// Task<T> is similar to Task, but you can retrieve a result from it. Task<double> piTask = Parallel.Start(CalculatePi); // Do something else for a while. DoSomethingElse(); // Retrieve the result. The caller will block until the task has completed. // GetResult() can only be called once! double pi = piTask.GetResult();
Long running operations which may block (i.e. wait for I/O operation to finish) should be scheduled as background tasks. Background tasks are created by using the method ParallelStartBackground. Background tasks will not be scheduled using the Scheduler (see below). Instead the class Parallel manages an additional pool of threads that are used for background tasks. The processor affinity of these threads is not set automatically. The background tasks will usually run on the same hardware thread where the background thread was created first or run last. The processor affinity can be set manually from within the task by calling Thread.SetProcessorAffinity.
// Begin loading some files in the background.
Parallel.StartBackground(LoadFiles);
The tasks executed asynchronously can raise exceptions. The exceptions are stored internally and a TaskException containing these exceptions is thrown when Wait is called.
It is possible to specify a completion callbacks when starting a new tasks. For example, see method ParallelStart(Action, Action). The completion callbacks are executed after the corresponding tasks have completed. Completion callbacks are executed regardless of whether tasks have completed successfully or have thrown an exception.
Important: The callbacks are not executed immediately! Instead, the method ParallelRunCallbacks needs to be called explicitly - usually on the main thread - to invoke the callbacks.
The following demonstrates how a for-loop can be executed in parallel.
// Sequential loop: for (int i = 0; i < count; i++) { DoWork(i); } // Same loop, but each iteration may happen in parallel on a different thread. Parallel.For(0, count, i => { DoWork(i); });
The following demonstrates how a foreach-loop can be executed in parallel.
// Sequential loop: foreach (var item in list) { DoWork(item); } // Same loop, but each iteration may happen in parallel on a different thread. Parallel.ForEach(list, item => { DoWork(item); });
The number of threads used for parallelization is determined by the task scheduler (see ParallelScheduler). The task scheduler creates a number of threads and distributes the tasks among these worker threads. The default task scheduler is a WorkStealingScheduler that creates one thread per CPU core on Windows and 3 threads on Xbox 360 (on the hardware threads 3, 4, and 5). The number of worker threads can be specified in the constructor of the WorkStealingScheduler.
The property Scheduler can be changed at runtime. The default task scheduler can be replaced with another task scheduler (e.g. with a WorkStealingScheduler that uses a different number of tasks, or with a custom ITaskScheduler). Replacing a task scheduler will affect all future tasks that have not yet been scheduled. However, it is highly recommended to use the default scheduler or specify the scheduler only once at the startup of the application.
In the .NET Compact Framework for Xbox 360 the processor affinity determines the processors on which a thread runs. Setting the processor affinity in Windows has no effect.
The processor affinity is defined as an array using the property ProcessorAffinity. Each entry in the array specifies the hardware thread that the corresponding worker thread will use. The default value is { 3, 4, 5, 1 }. The default task scheduler reads this array and assigns the worker threads to the specified hardware threads. (See also Thread.SetProcessorAffinity in the MSDN Library to find out more.)
Important: The processor affinity needs to be set before any parallel tasks are created or before a new WorkStealingScheduler is created. Changing the processor affinity afterwards has no effect.
// Configure the class Parallel to use the hardware threads 3 and 4 on the Xbox 360. // (Note: Setting the processor affinity has no effect on Windows.) Parallel.ProcessorAffinity = new[] { 3, 4 }; // Create task scheduler that uses 2 worker threads. Parallel.Scheduler = new WorkStealingScheduler(2); // Note: Above code is usually run at the start of an application. It is not recommended to // change the processor affinity or the task scheduler at runtime of the application.
The implementation is based on the ParallelTasks library (see http://paralleltasks.codeplex.com/) which is licensed under the Microsoft Public License (Ms-PL).
Tip |
---|
More background information about multithreading can be found in following article: Multithreading in the DigitalRune Engine |