Click here to Skip to main content
Licence CPOL
First Posted 4 Feb 2009
Views 25,715
Bookmarked 26 times

Poor Man's Parallel.ForEach Iterator

By | 4 Feb 2009 | Article
Parallel Task Library when released with .NET 4.0 will be great, until then here is a simple .NET 3.0 Parallel.ForEach for the rest of us.

Introduction

How about a simple parallel ForEach iterator that works with .NET 3.0 until Task Parallel Library is available with .NET 4.0.

Background

Task Parallel Library (TPL) will certainly help developers squeeze every bit of processing power out of their hardware, however Microsoft does not offer TPL for .NET 3.0 or .NET 3.5. TPL and Parallel LINQ will be offered as part of .NET 4.0. Until Microsoft releases .NET 4.0 or developers upgrade their development environments, here is a “Poor Man’s Parallel.ForEach” for the rest of us.

There is vast amount of literature about parallel processing, however in a nutshell I will try to explain the motivation for this code and offer my warnings: Reasonably priced laptops come with two CPU cores and average Windows servers have eight CPU cores. Even if you have ten thousand elements to iterate, a serial iteration such as foreach, while or for can only utilize single core where all the other cores are idling. If a particular procedure doesn't need to access objects that constantly share state, spreading processing over multiple threads provides proven performance gains. Being careful about objects sharing state is utterly important, if a coder blindly starts converting traditional serial iterations to parallel iterations; results will not be any different than giving a chain-saw to an 8 year old.

Using the Code

The usage is of the code is exactly as in TPL. Very simple and concise:

// Sample usage:   
var orders = GetOrders();
   Parallel.ForEach(orders, order => {
                                           if(order.IsValid)
                                             {
                                               order.Process();
                                             }
                                     });

Parallel.ForEach

I used asynchronous delegate invocation. Begin/End pattern is relatively simple to read and maintain. There is possibly an overhead using delegates over using System.Threads. Let me know how you can improve this code. I am looking forward to hear your comments.

public class Parallel
    {
        public static int NumberOfParallelTasks;
        
        static Parallel()
        {
            NumberOfParallelTasks = Environment.ProcessorCount;  
        }

        public static void ForEach<T>(IEnumerable<T> enumerable,Action<T> action)
        {
            var syncRoot = new object();

            if (enumerable == null ) return;

            var enumerator = enumerable.GetEnumerator();
                   
            InvokeAsync<T> del = InvokeAction;
           
            var seedItemArray = new T[NumberOfParallelTasks];
            var resultList = new List<IAsyncResult>(NumberOfParallelTasks);

            for (int i = 0; i < NumberOfParallelTasks; i++)
            {
                bool moveNext;
                
                lock (syncRoot)
                {
                    moveNext = enumerator.MoveNext();
                    seedItemArray[i] = enumerator.Current;
                }
            
                if (moveNext)
                {
                    var iAsyncResult= del.BeginInvoke
		     (enumerator, action, seedItemArray[i], syncRoot,i, null,null);
                    resultList.Add(iAsyncResult);                   
                }               
            } 
            
            foreach(var iAsyncResult in resultList)
            {
                del.EndInvoke(iAsyncResult); 
                iAsyncResult.AsyncWaitHandle.Close();
            }           
        }

        delegate void InvokeAsync<T>(IEnumerator<T> enumerator, 
		Action<T> achtion, T item, object syncRoot,  int i);

        static void InvokeAction<T>(IEnumerator<T> enumerator,Action<T> action,
				T item,object syncRoot,int i )
        {
            if(String.IsNullOrEmpty(Thread.CurrentThread.Name))
                Thread.CurrentThread.Name = 
			String.Format("Parallel.ForEach Worker Thread No:{0}", i);
            
            bool moveNext=true;

            while (moveNext)
            {
                action.Invoke(item);
                
                lock (syncRoot)
                {
                    moveNext = enumerator.MoveNext();
                    item = enumerator.Current;
                }
            }
        } 
    } 

Changes

There is always room for improvement. After publishing this article, I dug up other articles about BeginInvoke. Calling EndInvoke and AsyncWaitHandel.Close() is suggested not to leave garbage collection to chance. I tested it and it works fine and I didn't see performance overhead.

foreach (var iAsyncResult in resultList)iAsyncResult.AsyncWaitHandle.WaitOne();

The above changed to the following code:

foreach(var iAsyncResult in resultList)
            {
                del.EndInvoke(iAsyncResult); 
                iAsyncResult.AsyncWaitHandle.Close();
            }     

History

  • 4th February, 2009: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Emre Aydinceren

Architect
Harley-Davidson
United States United States

Member

Architect of Harley-Davidson's United States dealer's dealership management system.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
QuestionDoes it come in a V-Twin? PinmemberTheArchitectmc9:21 29 Dec '09  
GeneralPower Threading Library Pinmemberwallism19:25 10 Feb '09  
QuestionDoes the parrallel.For chop up the work in an optimal way or is it better to do this explicitly? PinmemberNigel-Findlater20:15 9 Feb '09  
AnswerRe: Does the parrallel.For chop up the work in an optimal way or is it better to do this explicitly? PinmemberEmre Aydinceren4:02 10 Feb '09  
Nigel,
 
I believe you are right, you have specialized algorithm and it is very likely when you put time in optimizing threading you will get better results than my Parallel.ForEach.
 
In my tests also I sometimes got better results if I used more threads than number of cores. My explanation to that is OS thread scheduler is also helping by utilizing any idle cycle within the iteration work. Even if we have 2 real cores, when we request OS to give us 16 threads it will give us and it will manage scheduling tasks between two real cores. If a single iteration work is idling (waiting for disk, memory or network I/O, graphics) OS will do a great job of utilizing that idle cycles for other threads.
 
Figuring optimum number of threads is my Parallel.ForEach's obvious weakness, I chose a conservative approach and used number of CPUs for that. In relation to that , another weakness of this code is that is totally agnostic of environment state and ambient tasks. For example if machine has 4 cores but 2 of them is busy, or two or more Parallel.ForEach statement are nested this code will overburden OS by too many unnecessary threads. That's why I am calling this "Poor Man's Parallel.FoEach" Smile | :)
 
I am open to any suggestions on how to make this code better.
 
Emre
GeneralProgram error [modified] PinmemberFrank Willett7:58 6 Feb '09  
AnswerRe: Program error PinmemberEmre Aydinceren17:48 8 Feb '09  
GeneralRe: Program error PinmemberFrank Willett8:48 9 Feb '09  
QuestionMultiple cores? PinmemberJosh Fischer2:39 5 Feb '09  
QuestionMight this be a problem with too many threads? Pinmemberargyler21:40 4 Feb '09  
AnswerRe: Might this be a problem with too many threads? Pinmemberassmax1:11 5 Feb '09  

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Mobile
Web03 | 2.5.120604.1 | Last Updated 4 Feb 2009
Article Copyright 2009 by Emre Aydinceren
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid