As suggested in comments here I created a new topic for next version. I edited a bit original code, added exception processing and optimization, that first N = 1/core count tasks are processed on main thread.
public static void Foreach<T>(ICollection<T> source, Action<T> action)
{
var toBeProcessedOnMainThread = (int) Math.Ceiling((double)source.Count / Environment.ProcessorCount);
var countdown = new CountdownEvent(source.Count - toBeProcessedOnMainThread);
ConcurrentBag<Exception> exceptionsBag = null;
var onMainThread = source.Take(toBeProcessedOnMainThread);
foreach (var item in source.Skip(toBeProcessedOnMainThread))
{
ThreadPool.QueueUserWorkItem(state =>
{
var closure = (T)state;
try
{
action(closure);
}
catch (Exception ex)
{
HandleException(ref exceptionsBag, countdown, ex);
}
finally
{
countdown.Signal();
}
}, item);
}
foreach (T value in onMainThread)
{
try
{
action(value);
}
catch (Exception ex)
{
HandleException(ref exceptionsBag, countdown, ex);
}
}
countdown.Wait();
if (exceptionsBag != null)
throw new AggregateException(exceptionsBag);
}
private static void HandleException(ref ConcurrentBag<Exception> exceptions, object syncRoot, Exception ex)
{
if (exceptions == null)
lock (syncRoot)
if (exceptions == null)
exceptions = new ConcurrentBag<Exception>();
exceptions.Add(ex);
}
I do lock countdown
because I do not want to introduce another local variable, and it locks only once and only if exception occurs. Normally it won't be called an thus won't be locked.
Parallel.ForEach()
instead of directly invoking threads. \$\endgroup\$