11
\$\begingroup\$

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();
    }
}
\$\endgroup\$
  • \$\begingroup\$ You could replace the foreach on resetEvents with 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\$ – Dan Lyons May 2 '14 at 17:53
  • 1
    \$\begingroup\$ Your wish is granted: nuget.org/packages/TaskParallelLibrary \$\endgroup\$ – Jesse C. Slicer May 2 '14 at 18:23
  • 1
    \$\begingroup\$ unfortunately, i cannot use external libraries (i could, but it would be a big hassle) \$\endgroup\$ – Lukas Rieger May 2 '14 at 18:42
  • 2
    \$\begingroup\$ The package Task Parallel Library for .NET 3.5 1.0.2856 that Jessie referred to is published by Microsoft. From the nuget page: "This backport was shipped with the Reactive Extensions (Rx) library up until v1.0.2856.0. It can be downloaded from microsoft.com/download/en/details.aspx?id=24940" \$\endgroup\$ – Phil Bolduc May 2 '14 at 20:57
  • 2
    \$\begingroup\$ The NuGet package contains one assembly, System.Threading.dll, which is signed by Microsoft and has a PublicKeyToken=31bf3856ad364e35 (same as System.Workflow.Activities). I guess you need to evaluate if the hassle of getting permission to use this 'external' library out weighs the hassle of writing your own. \$\endgroup\$ – Phil Bolduc May 2 '14 at 21:06
3
\$\begingroup\$
   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...

\$\endgroup\$
  • 1
    \$\begingroup\$ Credit for point out the TaskParallelLibrary package goes to @Jesse C. Slicer \$\endgroup\$ – Phil Bolduc May 3 '14 at 0:18
  • 3
    \$\begingroup\$ The point of ThreadPool is that it doesn't spawn a new thread for each operation. \$\endgroup\$ – svick May 3 '14 at 16:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.