Because the TPL backport not only contains Parallel.* but also PLINQ, concurrent collections, etc, I will try to use this one.
Just for fun, I profiled both the TPL implementation and my own, in the very basic and naive test, mine was actually a little bit faster.
Microsoft TPL backport: 00:00:17.0673098
My Implementation: 00:00:16.9210535
Intel Q6600 (quad-core), 4GB of RAM, Win8.1(x64), VS2013 targetting .NET 3.5
The code is here (github gist), the colorful output of the Concurrency Visualizer is below:
enter image description here
This negligeable performance difference (in the best case) gets outweight by the many optimisations the .NET team put in for special cases.
Question:
To ease working with multithreaded code, I wanted to implement a version of Parallel.ForEach
for .NET 3.5.
We are for the moment stuck with 3.5 and cannot upgrade. We cannot use external libraries like TPL.
I found this via Google, but this threw sporadic exceptions during runtime, and because I couldn't find another Version I decided to roll my own.
I did of course test it, but i am always a bit cautious with multithreaded code.
public class Parallel
{
public static void ForEach<T>(IEnumerable<T> items, Action<T> action)
{
if (items == null)
throw new ArgumentNullException("enumerable");
if (action == null)
throw new ArgumentNullException("action");
var resetEvents = new List<ManualResetEvent>();
foreach (var item in items)
{
var evt = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem((i) =>
{
action((T)i);
evt.Set();
}, item);
resetEvents.Add(evt);
}
foreach (var re in resetEvents)
re.WaitOne();
}
}
1 Answer 1
if (items == null) throw new ArgumentNullException("enumerable");
In the execution path where this exception is thrown, the reported argument name will not match the actual argument name. If this is the result of a rename refactoring (i.e. it's the parameter formerly known as "enumerable"), it's unfortunate, the string literal wasn't taken care of. That doesn't happen with ReShaper performing the refactoring, but if you're using the refactoring built into VS, I'm not sure it handles string literals; if you've manually "renamed" the argument, ...next time, use refactor/rename! ;)
Your implementation queues a work item for each item in items
.
ThreadPool.QueueUserWorkItem
Queues a method for execution. The method executes when a thread pool thread becomes available.
The interesting bit is about its return value:
Return Value
Type:
System.Boolean
true if the method is successfully queued;
NotSupportedException
is thrown if the work item could not be queued.
There's the slight possibility that thread pooling might not be supported on the platform code is running on (from what I gathered of reasons for this exception to be thrown), in which case the first iteration will throw a NotSupportedException
that the ForEach
code might not be expecting at all... but letting it bubble up might just be the best thing to do in this case.
If I correctly understand what's going on, you're essentially spawning a thread to run an action
, for each item
in items
. This might not be as efficient as you'd think it is: there's only so many concurrent operations that can happen on a quad-core processor, and then there's overhead too; sometimes it's more efficient to simply run the tasks sequentially.
And that's what Parallel.ForEach
does:
Parallel.ForEach
Executes a foreach operation on an
IEnumerable
in which iterations may run in parallel.
Thus, I'd use your implementation with care, and perhaps use a profiler and benchmark against a "normal" foreach
for a given number of iterations, but then your dev environment might have different computing power than your test and/or production environments, so careful with benchmarks, too.
It might be simpler to use the Microsoft NuGet package @JesseC.Slicer and @PhilBolduc mentioned in the comments...
-
1\$\begingroup\$ Credit for point out the TaskParallelLibrary package goes to @Jesse C. Slicer \$\endgroup\$Phil Bolduc– Phil Bolduc2014年05月03日 00:18:54 +00:00Commented May 3, 2014 at 0:18
-
3\$\begingroup\$ The point of
ThreadPool
is that it doesn't spawn a new thread for each operation. \$\endgroup\$svick– svick2014年05月03日 16:27:23 +00:00Commented May 3, 2014 at 16:27
WaitHandle.WaitAll(resetEvents.ToArray())
. See WaitHandle.WaitAll. It even lets you provide a timeout if you need to support actions which could never terminate. \$\endgroup\$