16.05.2010 at
10:05 pm · Saved under
.NET Help
Dear Parallel Computing Enthusiasts,
This week, Microsoft is rolling out a platform update to all blogs on the http://blogs.msdn.com site, including this one. Comments on this blog will be disabled at approximately 9pm PST tonight and will be off until mid-day on the 24th. We also do not expect to publish any new blog posts during this time. If you want to read a bit about some of the platform improvements that will be coming, Sean Jenkin’s blog is a good resource.
Thanks for your patience during this update. See you in a week!
Tagged: default-aspx • programming • wikipedia
04.05.2010 at
8:58 pm · Saved under
.NET Help
(The full set of ParallelExtensionsExtras Tour posts is available here.)
The Task Parallel Library provides the Task.Wait method, which synchronously waits for the target Task to complete. If the Task completed successfully, the method simply returns. If the Task completed due to an unhandled exception or cancellation, Wait throws an appropriate exception to connote that you can’t just blindly continue expecting the results or side-effects of the Task to have taken place.
While this is a very useful form of waiting, there are others that can be beneficial in certain situations, and ParallelExtensionsExtras includes a few different forms in the TaskExtrasExtensions.cs file.
WaitWithPumping
In a Windows Presentation Foundation application, especially when doing unit testing, you sometimes need to wait for a task on the UI thread. However, in doing so you don’t want to block the UI thread, and instead you want to continue the WPF message loop, maintaining a responsive application. For this purpose, ParallelExtensionsExtras includes the WaitWithPumping extension method for Task.
public static void WaitWithPumping(this Task task)
{
if (task == null) throw new ArgumentNullException(“task”);
var nestedFrame = new DispatcherFrame();
task.ContinueWith(_ => nestedFrame.Continue = false);
Dispatcher.PushFrame(nestedFrame);
task.Wait();
}
WaitWithPumping enters a message loop that will only exit when the task completes, which WPF knows about through a continuation applied to the task. Once the message loop has exited, we Wait on the task simply to propagate exceptions in the case where the task did not complete successfully.
With that in place, you could write WPF code like the following:
private void button1_Click(object sender, RoutedEventArgs e)
{
var t = Task.Factory.StartNew(() => Thread.Sleep(5000));
t.WaitWithPumping(); // UI remains responsive during call
MessageBox.Show(t.Status.ToString()); // will show “RanToCompletion”
}
Even thought Task t won’t complete for 5 seconds, and even though WaitWithPumping will block, the UI will still remain responsive. Then, because we waited for the Task to complete before exiting the message loop, the MessageBox will show “RanToCompletion”.
WaitForCompletionStatus
Sometimes you want to wait for a Task, but you don’t want the Wait operation to throw an exception, even if the target task completed in the Faulted or Canceled states. To achieve this, ParallelExtensionsExtras provides the WaitForCompletionStatus extension method on Task:
public static TaskStatus WaitForCompletionStatus(this Task task)
{
if (task == null) throw new ArgumentNullException(”task”);
((IAsyncResult)task).AsyncWaitHandle.WaitOne();
return task.Status;
}
This method relies on the fact that Task implements IAsyncResult, and thus implements the AsyncWaitHandle property. AsyncWaitHandle returns a WaitHandle that will be set when the task completes, and waiting on this wait handle will not throw exceptions in the same manner as does waiting on the task directly.
WaitForCompletionStatus returns the final TaskStatus of the task that was waited on. This makes it easy to write code that switches on a task’s completion state in order to do appropriate follow-up processing, e.g.
switch(task.WaitForCompletionStatus)
{
case TaskStatus.RanToCompletion:
Console.WriteLine(“Woo hoo!”);
break;
case TaskStatus.Faulted:
Console.WriteLine(“Uh oh: “ + task.Exception.Message);
break;
case TaskStatus.Canceled:
Console.WriteLine(“Oh well.”);
break;
}
Tagged: -net-4 • default-aspx • parallel-extensions • task- • task-parallel-library
28.04.2010 at
9:00 am · Saved under
.NET Help
(The full set of ParallelExtensionsExtras Tour posts is available here.)
In a previous ParallelExtensionsExtras Tour blog post, we talked about implementing a custom partitioner for BlockingCollection<T>. Custom partitioning is an advanced but important feature supported by both Parallel.ForEach and PLINQ, as it allows the developer full control over how data is distributed during parallel processing. .NET 4 includes several high-quality partitioning algorithms that are built-in and used by default, but sometimes you do want to tweak the employed behaviors, and that’s where the Partitioner<T> and OrderablePartitioner<T> types come in.
For example, the built-in partitioning algorithms do chunking in order to amortize the cost of synchronization across multiple data elements. For some scenarios, however, that chunking could lead to less-than-ideal behavior, and thus you might want to write a custom partitioner that doesn’t do any chunking, that instead always hands out a single data element at a time to whichever thread is ready next to process more data. ParallelExtensionsExtras includes just such a partitioner, SingleItemPartitioner, in the SingleItemPartitioner.cs file. Rather than explain this type in detail here, I’ll instead point you to a new article in Dr. Dobbs “Custom Parallel Partitioning in .NET 4“ that walks through the principles of custom partitioning and SingleItemPartitioner’s implementation in detail.
Enjoy!
Tagged: -net-4 • cds • code-samples • default-aspx • plinq
27.04.2010 at
7:49 am · Saved under
.NET Help
(The full set of ParallelExtensionsExtras Tour posts is available here.)
Producer/consumer scenarios could logically be split into two categories: those where the consumers are synchronous, blocking waiting for producers to generate data, and those where the consumers are asynchronous, such that they’re alerted to data being available and only then spin up to process the produced data. BlockingCollection<T> is very useful for the former case, as it encapsulates patterns that simplify the implementation of a blocking consumer. BlockingCollection<T> is not as ideal for cases where the consumer is asynchronous.
For asynchronous producer/consumer, typically the producer adds data to a shared data structure, such as a thread-safe queue, and notifies any listening consumers that data is available. This then causes the consumer to check the queue, pull out the relevant data, and process it. There are a variety of choices one has to make when applying this approach. Should data be processed in order? Should parallelism be employed to process multiple data elements concurrently if possible? etc.
The AsyncCall<T> type in the AsyncCall.cs file in ParallelExtensionsExtras implements this producer/consumer functionality. The programming model is quite straightforward. You instantiate a new AsyncCall<T> with the delegate to be executed for each piece of data, e.g.:
var call = new AsyncCall<int>(i =>
{
Console.WriteLine(DoWork(i));
});
and then you post data to the instance, e.g.:
call.Post(1);
call.Post(2);
As is common in producer/consumer scenarios, consumers may themselves be producers for later stages of a pipeline, and in that fashion you can chain together AsyncCall<T> instances by having the delegate for one post to another, e.g.:
AsyncCall<double> call1 = null;
AsyncCall<string> call2 = null;
AsyncCall<int> call3 = null;
call1 = new AsyncCall<double>(d => call2.Post(d.ToString()));
call2 = new AsyncCall<string>(s => call3.Post(s.Length));
call3 = new AsyncCall<int>( i => Console.WriteLine(i));
call1.Post(1.1);
call1.Post(2.2);
call1.Post(3.3);
call1.Post(4.4);
Under the covers, AsyncCall<T> is implemented using Tasks, a fact evident from the AsyncCall<T>’s constructor:
public AsyncCall(
Action<T> actionHandler,
int maxDegreeOfParallelism = 1,
int maxItemsPerTask = Int32.MaxValue,
TaskScheduler scheduler = null);
The constructor not only accepts the action delegate to be executed for each item, but also the maximum number of delegates to run concurrently (by default, it executes sequentially), the maximum number of items to be processed per Task (more on this in a moment), and the TaskScheduler to which to schedule tasks. There are other quite interesting constructors on the type but not detailed here, including one that uses a Func<T,Task> instead of an Action<T>. This allows a Task to be returned from the handler, where the handler won’t be considered completed until that task has asynchronous completed. This allows AsyncCall<T> to be used in cases where, for example, you want to employ asynchronous I/O to process data, but you also want to ensure that no more than one item is being processed at a time.
AsyncCall<T>’s implementation is relatively simple. When Post is called to provide new data, that data is enqueued into an internal ConcurrentQueue<T>. The instance then checks to see whether a Task has been scheduled to process data: if one hasn’t, we schedule such a Task. This check enables us to minimize the number of tasks created, such that one Task can process all of the data posted while it happens to be running. When no more data is available, the task will exit, and the next piece of data to be posted will cause another Task to spin up and continue processing. If maxDegreeOfParallelism is set to something greater than 1, the body of the Task will employ a Parallel.ForEach loop to process available data instead of using a normal serial foreach loop.
I mentioned earlier that the AsyncCall<T>’s constructor accepts a maxItemsPerTask parameter. This is used for fairness. If continuous calls to Post were made such that the Task doing the underlying processing always had more data available, this Task would never exit. This could cause starvation problems for other work that needs some time to execute. To deal with this, the Task employed by AsyncCall<T> exits after processing maxItemsPerTask items, even if there’s more to be done. On its way out, the Task queues up another Task to complete the processing, and that task may then be scheduled fairly with regards to other work to be done.
Tagged: agents • default-aspx
23.04.2010 at
11:42 am · Saved under
.NET Help
(The full set of ParallelExtensionsExtras Tour posts is available here.)
Caches are ubiquitous in computing, serving as a staple of both hardware architecture and software development. In software, caches are often implemented as dictionaries, where some data is retrieved or computed based on a key, and then that key and its resulting data/value are added to the dictionary. Prior to re-retrieving or re-computing the value for a given key, we can first check the dictionary/cache to see whether we’ve already done so, and if we have, we simply copy the element from the dictionary.
As we all know, a multithreaded environment can bring with it many challenges, and such challenges apply to caches as well. Imagine creating a cache to store downloaded web pages. If multiple threads are trying to access the cache at the same time, we not only want to make sure that they don’t corrupt the employed data structures, we also want to make sure that they’re not doing more work than they need to: if two threads need the same page downloaded, just download it once rather than twice, and give them both copies. In this fashion, we need a form of an asynchronous cache that allows threads to get back a handle for the thing in the cache they want, a handle that, for example, will then provide them with a callback notification when the download has completed or that will allow them to wait for the download to complete.
The AsyncCache class in AsyncCache.cs in ParallelExtensionsExtras provides this support, and it may surprise you just how little code is required to do this, taking advantage of the new concurrency support in .NET 4. The type is defined as follows:
public class AsyncCache<TKey, TValue>
and contains two fields. The first field is a delegate that will be invoked for a key when that key is requested and is not yet in the dictionary; it is this delegate that produces the value for the key:
private readonly Func<TKey, Task<TValue>> _valueFactory;
Note that this isn’t a Func<TKey,TValue>, but rather a Func<TKey,Task<TValue>>. The function is supplied by the user to the AsyncCache constructor and produces a task that represents the retrieval of the value for a given key. This task could either be computational in nature (e.g. one created by Task.Factory.StartNew), or it could be async I/O-based, such as a task representing a download from a web site. Either way, it’s this task that’s stored in the cache in the second field:
private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map;
As you might have guessed, we’re using a ConcurrentDictionary as the storage for the cache, which helps to ensure that multiple threads may access the cache concurrently without corrupting the internals of the data store. As noted earlier, the function to generate values for keys returns tasks, but the dictionary’s value isn’t just Task<TValue>, it’s Lazy<Task<TValue>>. The addition of the Lazy<> here makes it really easy to ensure that only one task is generated for any one key, avoiding any races that might otherwise result. We can see this by looking at the most important method on AsyncCache:
public Task<TValue> GetValue(TKey key)
{
var value = new Lazy<Task<TValue>>(() => _valueFactory(key));
return _map.GetOrAdd(key, value).Value;
}
You’ve now seen almost all of AsyncCache’s implementation… everything else in the type is really secondary (e.g. implementing the ICollection interface). GetValue simply creates a new Lazy<Task<TValue>> that will run the _valueFactory when invoked. The method then checks whether the dictionary already has a Lazy<> for this key, adding the one we just created if it didn’t yet have one, and regardless returning the Value of whatever Lazy<> we got back. By accessing the Lazy<Task<TValue>>’s Value, we get back the task for this key, and that’s handed back to the caller. The caller now has a Task<TValue> for the supplied TKey, and as with any other task, the caller can use ContinueWith to be notified when the task has completed, can Wait on the task to block until the task has completed, or can simply use its Result property to get at the data when it’s available (potentially blocking in the process).
With AsyncCache<TKey,TValue> in place, it’s now straightforward to either use it as is, or to create specialized variants of the cache. For example, in our earlier problem statement we described wanting to be able to cache downloaded web pages. Here’s the complete implementation of that:
public sealed class HtmlAsyncCache : AsyncCache<Uri, string>
{
public HtmlAsyncCache() :
base(uri => new WebClient().DownloadStringTask(uri)) { }
}
The DownloadStringTask extension method on WebClient is another method defined in ParallelExtensionExtras, and we’ll get to that another day. Suffice it to say that this method returns a Task<string> that represents the asynchronous downloading of a web page at the specified Uri. As such, our HtmlAsyncCache is simply a derived AsyncCache<Uri,string>, where the valueFactory calls DownloadStringTask for the supplied key/uri.
A consumer of this type may request a particular page:
HtmlAsyncCache cache = new HtmlAsyncCache();
…
Task<string> page =
cache.GetValue(new Uri(“http://www.microsoft.com”));
and then either use its value directly, blocking if it’s not yet available:
Console.WriteLine(page.Result);
or ask to be notified when the value is available:
page.ContinueWith(completed =>
Console.WriteLine(completed.Result));
And if you wanted to download multiple pages and only do something when you had all three, that’s easy as well. Since our asynchronous operations are represented as tasks, we can use the combinators provided by Task for this purpose, e.g.
var page1 = cache.GetValue(
new Uri(“http://msdn.microsoft.com/pfxteam”));
var page2 = cache.GetValue(
new Uri(“http://msdn.com/concurrency”));
var page3 = cache.GetValue(
new Uri(“http://www.microsoft.com”));
Task.Factory.ContinueWhenAll(
new [] { page1, page2, page3 }, completedPages =>
{
… // use the downloaded pages here
});
(Thanks go to Luca Bolognese for originally supplying the idea for AsyncCache.)
Tagged: -net-4 • cds • default-aspx • parallel-extensions • task-
21.04.2010 at
11:00 am · Saved under
.NET Help
If you’ve played around with PLINQ and Parallel.ForEach loops in .NET 4, you may have noticed that many PLINQ queries can be rewritten as parallel loops, and also many parallel loops can be rewritten as PLINQ queries. However, both parallel loops and PLINQ have distinct advantages in different situations. When writing parallel code, it helps to understand these differences to appropriately decide between the two models.
Pamela Vagata wrote an interesting comparison of parallel loops and PLINQ: When Should I Use Parallel.ForEach? When Should I Use PLINQ?
(This paper and many more are available through the Parallel Computing Developer Center on MSDN at http://msdn.microsoft.com/en-us/concurrency/ee851578.aspx.)
Tagged: -net-4 • article-summary • default-aspx • parallel-extensions • plinq
14.04.2010 at
1:06 am · Saved under
.NET Help
We’ve spent a lot of time touting improvements to the .NET Framework in .NET 4 around threading, including core enhancements to the performance of the runtime itself. Sometimes data is more powerful than words, however, and it’s useful to be able to see exactly what kind of difference such improvements can make. To assist with that, here is code for a small sample you can try compiling and running on your own:
using System;
using System.Diagnostics;
using System.Threading;
class Program
{
static void Main(string[] args)
{
Console.WriteLine(
TimeSpan.FromMilliseconds(
Enumerable.Range(0, 6).Select(_ =>
{
var sw = Stopwatch.StartNew();
CreateAndWaitForWorkItems(10000000);
return sw.ElapsedMilliseconds;
}).Skip(1).Average()
)
);
}
static void CreateAndWaitForWorkItems(int numWorkItems)
{
using (ManualResetEvent mre = new ManualResetEvent(false))
{
int itemsRemaining = numWorkItems;
for (int i = 0; i < numWorkItems; i++)
{
ThreadPool.QueueUserWorkItem(delegate
{
if (Interlocked.Decrement(
ref itemsRemaining) == 0) mre.Set();
});
}
mre.WaitOne();
}
}
}
The CreateAndWaitForWorkItems method simply launches N work items using ThreadPool.QueueUserWorkItem and then waits for all N to complete by atomically decrementing a shared counter. The main method then times the invocation of this method with N equal to 10 million, doing so several times and taking the average. This microbenchmark is pure overhead, as there’s no actual work being performed in each work item. In fact, we should expect that as we add more cores, the time to complete this operation will increase, as more cores will contend for the data structures employed in both the ThreadPool and in my simple test. The hope is that the work done in .NET 4 decreases that overhead, especially on higher core counts where more and more threads will be contending for the shared data structures employed.
The following numbers are in no way official benchmarks, but they can give you a sense for how the work that’s been done in .NET 4 really does make a difference. These are the numbers I see when I run this microbenchmark informally on .NET 3.5 and on .NET 4 on two laptops I currently have access to while writing this blog post. The only change I made to go from .NET 3.5 to .NET 4 was modifying the “Target framework” in the project’s properties in Visual Studio, taking advantage of Visual Studio 2010’s multitargeting support.
Machine |
.NET 3.5 |
.NET 4 |
Improvement |
A dual-core box |
5.03 seconds |
2.45 seconds |
2.05x |
A quad-core box |
19.39 seconds |
3.42 seconds |
5.67x |
Some pretty awesome performance improvements simply by upgrading to .NET 4.
Tagged: .net • default-aspx • parallel • silverlight • threadpool
12.04.2010 at
1:07 pm · Saved under
.NET Help
In coordination with today’s launch of Visual Studio 2010 and the .NET Framework 4, several case studies have been published on how companies are taking advantage of parallel computing with the .NET Framework to improve the performance of their applications. It’s exciting to see the benefits these companies are seeing and how parallelism is so positively affecting their businesses:
ORElogy
Mine-Planning Software Uses Parallelization to Deliver Powerful New Capabilities
“In developing its own mine-planning software, consultancy ORElogy needed to capitalize on the performance gains enabled by multicore processors and multiprocessor servers, but it wanted to avoid having to write a lot of low-level threading code. The company used the parallel-programming aids provided in the Microsoft Visual Studio 2010 development system and the .NET Framework 4 to achieve the performance needed to support geologic models consisting of millions of individual blocks of earth—an order of magnitude more than the practical limits imposed by many competing products. Through its use of parallel programming, the company is delivering compelling new capabilities for customers. And because ORElogy is using familiar tools, virtually no additional learning was required to parallelize its code.”
Composite
Content Management Solution Uses Parallelization to Deliver Huge Performance Gains
“Content management system vendor Composite needed to parallelize its software to realize the performance gains enabled by today’s multicore processors. Composite took advantage of the new parallel-programming tools provided in the Microsoft Visual Studio 2010 development system and the .NET Framework 4 to parallelize its code. The company’s efforts have yielded impressive performance gains: An eight-core server is delivering a 60 percent reduction in page-rendering times and an 80 percent reduction in the compilation of dynamic types upon system initialization. What’s more, by using the latest Microsoft aids for parallel programming, Composite was able to implement parallelism in its solution quickly and cost-effectively, with very little developer effort.”
Grange Insurance
Insurance Company Uses Parallel Programming to Move Rating Engine off Mainframe
“From 1999 to 2010, Grange Insurance maintained its mission-critical rating engine in two places: a Windows-based Web site for agents, and a mainframe for heavy batch processing. To eliminate duplicate effort, Grange Insurance is consolidating all rating activity onto the Windows Server operating system environment—made possible by using the new parallel-programming aids provided in the Microsoft Visual Studio 2010 development system and the .NET Framework 4 to achieve performance gains of up to 750 percent. Consolidation of all rating activity on Windows Server will reduce the company’s annual IT workload by 20,000 hours and decrease cycle times for rate adjustments by two-thirds, from three months to one. The company’s use of Microsoft technology helped developers parallelize the code quickly and easily, with minimal effort and virtually no learning curve.”
Visual Numerics
Numerical Library Provider Realizes Major Performance Gains by Parallelizing Its Code
“Visual Numerics needed a way to efficiently parallelize its C# numerical library software—and thus realized the performance gains made possible by today’s multicore processors. The company continues to improve the performance of its IMSL C# Numerical Libraries by taking advantage of the support for parallel programming provided in the Microsoft Visual Studio 2010 development system and the Microsoft .NET Framework 4. This support enables Visual Numerics developers to productively write scalable parallel code at higher levels of abstraction than they could by working directly with threads. The company’s efforts are yielding an almost linear increase in performance on multicore systems, positioning Visual Numerics as an industry leader in meeting the needs of Windows developers working in the field of technical computing.”
Tagged: -net-4 • case-studies • default-aspx • microsoft • wikipedia
12.04.2010 at
12:55 pm · Saved under
.NET Help
As announced here, Visual Studio 2010 and the .NET Framework 4 are available! This includes the exciting support for parallel computing that we’ve been talking about for quite some time now. For an overview of what’s new for developers with managed code, both in terms of frameworks and tools, check out this new article on DevProConnections: Parallel Computing with the .NET Framework 4.
Tagged: -net-4 • default-aspx • tools • visual-studio
09.04.2010 at
12:10 pm · Saved under
.NET Help
(The full set of ParallelExtensionsExtras Tour posts is available here.)
In our last two ParallelExtensionsExtras blog tour posts, we’ve discussed two TaskScheduler implementations in ParallelExtensionsExtras: StaTaskScheduler and ConcurrentExclusiveInterleave. These are just 2 of more than 10 schedulers in ParallelExtensionsExtras, and rather than explore the rest in-depth, in this post we’ll provide a short description of other key schedulers included.
QueuedTaskScheduler
Found in the QueuedTaskScheduler.cs file, QueuedTaskScheduler provides a wealth of functionality all wrapped up into a single scheduler type. It supports:
· Priorities. You create a queue of off the scheduler with a particular priority, and that queue is itself a TaskScheduler. Any tasks you schedule to that scheduler then are tagged with that priority, and the scheduler will service tasks in priority order. e.g.
QueuedTaskScheduler qts = new QueuedTaskScheduler();
TaskScheduler pri0 = qts.CreateQueue(priority:0);
TaskScheduler pri1 = qts.CreateQueue(priority:1);
Any tasks scheduled to pri0 will get priority over tasks scheduled to pri1, even if scheduled after.
· Fairness. It’s often the case that you have batches of work, and you want each of these batches to be treated fairly with each other, such that if a large batch arrives and then a small batch arrives, the small batch doesn’t have to wait until the entire large batch is completed; instead, tasks from both the large and small batches will be scheduled fairly and round-robin’d between. The queues created on a QueuedTaskScheduler are scheduled in just such a manner, also taking into account priorities. e.g.
QueuedTaskScheduler qts = new QueuedTaskScheduler();
TaskScheduler pri0a = qts.CreateQueue(priority:0);
TaskScheduler pri0b = qts.CreateQueue(priority:0);
TaskScheduler pri1 = qts.CreateQueue(priority:1);
Tasks scheduled to both pri0a and pri0b will take priority over tasks scheduled to pri1. However, tasks scheduled to pri0a and pri0b will be round-robin’d between, as they exist at the same priority level.
· Concurrency Levels. In a large system, you may want to control how much parallelism is afforded to different parts of the system. With parallel loops and PLINQ queries, you can control this on a per-loop or per-query basis, but out-of-the-box there’s no way to control it across loops, and there’s no built-in way to control it for tasks. By scheduling all related work to a TaskScheduler that enforces a maximum concurrency level, that functionality is gained.
var qts = new QueuedTaskScheduler(
TaskScheduler.Default, maxConcurrencyLevel:4);
var options = new ParallelOptions { TaskScheduler = qts };
Task.Factory.StartNew(() =>
{
Parallel.For(0, 100, options, i=>
{
…
});
}, CancellationToken.None, TaskCreationOptions.None, qts);
Task.Factory.StartNew(() =>
{
Parallel.For(0, 100, options, i=>
{
…
});
}, CancellationToken.None, TaskCreationOptions.None, qts);
Both tasks and the parallel loops they contain will be limited to a maximum concurrency level of four.
· Thread control. The priorities, fairness, and concurrency level control all apply when QueuedTaskScheduler is used on top of another TaskScheduler as well as when used with dedicated threads for the scheduler. However, QueuedTaskScheduler also provides very low-level control over the threads utilized by the scheduler when dedicated threads are requested. Here’s the relevant constructor, which provides insight into the various knobs provided:
public QueuedTaskScheduler(
int threadCount,
string threadName = “”,
bool useForegroundThreads = false,
ThreadPriority threadPriority = ThreadPriority.Normal,
ApartmentState threadApartmentState = ApartmentState.MTA,
int threadMaxStackSize = 0,
Action threadInit = null,
Action threadFinally = null);
IOTaskScheduler
While we often refer to “the” .NET thread pool, the ThreadPool abstraction in .NET is actually built on top of two pools, one referred to as the worker pool and one referred to as the I/O pool. The former is what’s targeted by the QueueUserWorkItem method as well as by the default TaskScheduler, while the latter is targeted by the UnsafeQueueNativeOverlapped method, and is frequently used for work in Windows Communication Foundation and Windows Workflow Foundation. The IOTaskScheduler scheduler in the IOTaskScheduler.cs file runs tasks on this I/O thread pool via the UnsafeQueueNativeOverlapped method.
WorkStealingTaskScheduler
In .NET 4, the ThreadPool was augmented with “work-stealing” algorithms. However, the ThreadPool does not allow you to create instances of pools, only to share the global pool. If you desire individual pool instances with a dedicated set of threads, and if you also want work-stealing, the WorkStealingTaskScheduler class in the WorkStealingTaskScheduler.cs file is your friend.
SynchronizationContextTaskScheduler
The TaskScheduler.FromCurrentSynchronizationContext static method returns a TaskScheduler instance that wraps the SynchronizationContext instance returned from SynchronizationContext.Current. In rare circumstances, however, you might want to wrap an arbitrary SynchronizationContext that is not set as the current context. SynchronizationContextTaskScheduler.cs contains a scheduler that does exactly that. When a Task is queued to this scheduler, it’s added to an internal concurrent queue, and a delegate is then posted to the target SynchronizationContext, which was passed into the scheduler’s constructor:
protected override void QueueTask(Task task)
{
_tasks.Enqueue(task);
_context.Post(delegate
{
Task nextTask;
if (_tasks.TryDequeue(out nextTask)) TryExecuteTask(nextTask);
}, null);
}
We could have skipped storing the task, and instead passed it directly to the delegate via a closure. We opted for the shown approach, however, in order to enable debugger integration, so that the GetScheduledTasks method can return the contents of the queue:
protected override IEnumerable<Task> GetScheduledTasks()
{
return _tasks.ToArray();
}
ThreadPerTaskScheduler
In rare cases, it’s desirable to dedicate a thread to each individual task, rather than having tasks share a pool of threads. For this, the ThreadPerTaskScheduler.cs file contains a very simple ThreadPerTaskScheduler class that does exactly what its name implies, spinning up a dedicated thread for each task that gets queued to the scheduler. Here is its QueueTask method:
protected override void QueueTask(Task task)
{
new Thread(() => TryExecuteTask(task)) { IsBackground = true }.Start();
}
CurrentThreadTaskScheduler
Largely for testing purposes, a synchronous scheduler is provided in CurrentThreadTaskScheduler.cs that runs all tasks on the current thread when scheduling is requested. The implementation is trivial. Here is its QueueTask method:
protected override void QueueTask(Task task)
{
TryExecuteTask(task);
}
LimitedConcurrencyLevelTaskScheduler, OrderedTaskScheduler, and ReprioritizableTaskScheduler
The LimitedConcurrencyLevelTaskScheduler.cs, OrderedTaskScheduler.cs, and ReprioritizableTaskScheduler.cs files contain schedulers that are largely limited variants of the QueuedTaskScheduler class. LimitedConcurrencyLevelTaskScheduler sits on top of the ThreadPool and supports a user-provided maximum degree of concurrency. OrderedTaskScheduler derives from LimitedConcurrencyLevelTaskScheduler and simply sets that maximum degree of concurrency to one, which guarantees that tasks are processed in the order that they were scheduled. ReprioritizableTaskScheduler is a simple first-in-first-out scheduler on top of the ThreadPool, except that it supports moving tasks around in the queue after they’ve been queued; this effectively enables prioritizing and deprioritizing previously queued tasks.
Tagged: default-aspx • parallel • parallelextensionsextras • task-parallel-library • threadpool
Older entries » »