views:

596

answers:

4

My application spawns loads of different small worker threads via ThreadPool.QueueUserWorkItem which I keep track of via multiple ManualResetEvent instances. I use the WaitHandle.WaitAll method to block my application from closing until these threads have completed.

I have never had any issues before, however, as my application is coming under more load i.e. more threads being created, I am now beginning to get this exception:

WaitHandles must be less than or equal to 64 - missing documentation

What is the best alternative solution to this?

Code Snippet

List<AutoResetEvent> events = new List<AutoResetEvent>();

// multiple instances of...
var evt = new AutoResetEvent(false);
events.Add(evt);
ThreadPool.QueueUserWorkItem(delegate
{
    // do work
    evt.Set();
});

...
WaitHandle.WaitAll(events.ToArray());

Workaround

int threadCount = 0;
ManualResetEvent finished = new ManualResetEvent(false);

...
Interlocked.Increment(ref threadCount);
ThreadPool.QueueUserWorkItem(delegate
{
    try
    {
         // do work
    }
    finally
    {
        if (Interlocked.Decrement(ref threadCount) == 0)
        {
             finished.Set();
        }
    }
});

...
finished.WaitOne();
+7  A: 

Create a variable that keeps track of the number of running tasks:

int numberOfTasks = 100;

Create a signal:

ManualResetEvent signal = new ManualResetEvent(false);

Decrement the number of tasks whenever a task is finished:

if (Interlocked.Decrement(ref numberOftasks) == 0)
{

If there is no task remaining, set the signal:

    signal.Set();
}

Meanwhile, somewhere else, wait for the signal to be set:

signal.WaitOne();
dtb
@dtb: So rather than maintaining a list of MRE's just maintain the count and after each thread finishes just decrement (and trigger if count reaches 0)?
James
Exactly. MRE's are somewhat heavyweight, so the 64 limit has a reason. Try to avoid too many of them :-)
dtb
Excellent, jst one more thing. I don't exactly know the number of threads the list is built up dynamically is it safe to increment the count inside each new thread before any work is performed and then decrement at the end?
James
No don't increment the numberOfTasks in the thread. That could lead to a situation where the numberOfTasks reaches 0 but not all tasks have been started yet. Increment the numberOfTasks right before your call to ThreadPool.QueueUserWorkItem, so it's guaranteed that all tasks have been started before you call signal.WaitOne(). Use Interlocked.Increment to increment the numberOfTasks.
dtb
@dtb: I was worried incase what happens if I increment the numberOfTasks and for (whatever reason) the thread fails to start, this means the signal would never get set or numberOfTasks would never get decremented? Not really sure of the likelyhood of this happening tho....
James
Any code in your task (`// do work`) can throw an exception. So you should make sure that, if an exception is thrown, the numberOfTasks is properly decremented (`try ... finally`). Don't worry that your UserWorkItem might not be executed - it will. Eventually.
dtb
@dtb: Actually just did that! haha will post the code I intend to use, thanks.
James
A: 

If and when you move to .NET 4 take a look at CountdownEvent. It wraps up the counting in a tidy package.

Curt Nichols
+2  A: 

Adding to dtb's answer you can wrap this into a nice simple class.

public class Countdown : IDisposable
{
    private readonly ManualResetEvent done;
    private readonly int total;
    private long current;

    public Countdown(int total)
    {
        this.total = total;
        current = total;
        done = new ManualResetEvent(false);
    }

    public void Signal()
    {
        if (Interlocked.Decrement(ref current) == 0)
        {
            done.Set();
        }
    }

    public void Wait()
    {
        done.WaitOne();
    }

    public void Dispose()
    {
        ((IDisposable)done).Dispose();
    }
}
ChaosPandion
A: 

Your workaround is not correct. The reason is that the Set and WaitOne could race if the last work item causes the threadCount to go to zero before the queueing thread has had to chance to queue all work items. The fix is simple. Treat your queueing thread as if it were a work item itself. Initialize threadCount to 1 and do a decrement and signal when the queueing is complete.

int threadCount = 1;
ManualResetEvent finished = new ManualResetEvent(false);
...
Interlocked.Increment(ref threadCount); 
ThreadPool.QueueUserWorkItem(delegate 
{ 
    try 
    { 
         // do work 
    } 
    finally 
    { 
        if (Interlocked.Decrement(ref threadCount) == 0) 
        { 
             finished.Set(); 
        } 
    } 
}); 
... 
if (Interlocked.Decrement(ref threadCount) == 0)
{
  finished.Set();
}
finished.WaitOne(); 

As a personal preference I like using the CountdownEvent class to do the counting for me.

var finished = new CountdownEvent(1);
...
finished.AddCount();
ThreadPool.QueueUserWorkItem(delegate 
{ 
    try 
    { 
         // do work 
    } 
    finally 
    { 
      finished.Signal();
    } 
}); 
... 
finished.Signal();
finished.Wait(); 
Brian Gideon