The purpose of this code is to let me loop over 100 items (up to MAX_CONCURRENT
at a time), performing some action on them, and then return only once all items have been processed:
/// <summary>Generic method to perform an action or set of actions
/// in parallel on each item in a collection of items, returning
/// only when all actions have been completed.</summary>
/// <typeparam name="T">The element type</typeparam>
/// <param name="elements">A collection of elements, each of which to
/// perform the action on.</param>
/// <param name="action">The action to perform on each element. The
/// action should of course be thread safe.</param>
/// <param name="MaxConcurrent">The maximum number of concurrent actions.</param>
public static void PerformActionsInParallel<T>(IEnumerable<T> elements, Action<T> action)
{
// Semaphore limiting the number of parallel requests
Semaphore limit = new Semaphore(MAX_CONCURRENT, MAX_CONCURRENT);
// Count of the number of remaining threads to be completed
int remaining = 0;
// Signal to notify the main thread when a worker is done
AutoResetEvent onComplete = new AutoResetEvent(false);
foreach (T element in elements)
{
Interlocked.Increment(ref remaining);
limit.WaitOne();
new Thread(() =>
{
try
{
action(element);
}
catch (Exception ex)
{
Console.WriteLine("Error performing concurrent action: " + ex);
}
finally
{
Interlocked.Decrement(ref remaining);
limit.Release();
onComplete.Set();
}
}).Start();
}
// Wait for all requests to complete
while (remaining > 0)
onComplete.WaitOne(10); // Slightly better than Thread.Sleep(10)
}
I include a timeout on the WaitOne()
before checking remaining
again to protect against the rare case where the last outstanding thread decrements 'remaining' and then signals completion between the main thread checking 'remaining' and waiting for the next completion signal, which would otherwise result in the main thread missing the last signal and locking forever. This is faster than just using Thread.Sleep(10)
because it has a chance to return immediately after the last thread completes.
Goals:
Ensure thread safety - I want to be sure I won't accidentally return too early (before all elements have been acted on), and be sure that I don't become deadlocked or otherwise stuck.
Add as little overhead as possible - minimizing amount of time that fewer than
MAX_CONCURRENT
threads are executingaction
, and returning as soon as possible after the finalaction
has been performed.
.AsParallel()
before. I gave it a shot, and it works well! \$\endgroup\$WithDegreeOfParallelism
\$\endgroup\$