views:

515

answers:

6

Perhaps it's too late at night, but I can't think of a nice way to do this.

I've started a bunch of asynchronous downloads, and I want to wait until they all complete before the program terminates. This leads me to believe I should increment something when a download starts, at decrement it when it finishes. But then how do I wait until the count is 0 again?

Semaphores sort of work in the opposite way in that you block when there are no resources available, not when they're all available (blocks when count is 0, rather than non-zero).


Based on the suggestions here, this is what I came up with. In addition to waiting until the count is 0, it will sleep if you spawn too many threads (count > max). Warning: This is not fully tested.

public class ThreadCounter
{
    #region Variables
    private int currentCount, maxCount;
    private ManualResetEvent eqZeroEvent;
    private object instanceLock = new object();
    #endregion

    #region Properties
    public int CurrentCount
    {
        get
        {
            return currentCount;
        }
        set
        {
            lock (instanceLock)
            {
                currentCount = value;
                AdjustZeroEvent();
                AdjustMaxEvent();
            }
        }
    }

    public int MaxCount
    {
        get
        {
            return maxCount;
        }
        set
        {
            lock (instanceLock)
            {
                maxCount = value;
                AdjustMaxEvent();
            }
        }
    }
    #endregion

    #region Constructors
    public ThreadCounter() : this(0) { }
    public ThreadCounter(int initialCount) : this(initialCount, int.MaxValue) { }
    public ThreadCounter(int initialCount, int maximumCount)
    {
        currentCount = initialCount;
        maxCount = maximumCount;
        eqZeroEvent = currentCount == 0 ? new ManualResetEvent(true) : new ManualResetEvent(false);
    }
    #endregion

    #region Public Methods
    public void Increment()
    {
        ++CurrentCount;
    }

    public void Decrement()
    {
        --CurrentCount;
    }

    public void WaitUntilZero()
    {
        eqZeroEvent.WaitOne();
    }
    #endregion

    #region Private Methods
    private void AdjustZeroEvent()
    {
        if (currentCount == 0) eqZeroEvent.Set();
        else eqZeroEvent.Reset();
    }

    private void AdjustMaxEvent()
    {
        if (currentCount <= maxCount) Monitor.Pulse(instanceLock);
        else do { Monitor.Wait(instanceLock); } while (currentCount > maxCount);
    }
    #endregion
}
+3  A: 

Well... you can snatch all the semaphore counters on the main thread back in order to blocks when count is 0, rather than non-zero.

REVISED: Here I assumed 3 things:

  • While the program is running, a new download job may start at any time.
  • On exiting the program, there will be no more new downloads that needs taken care of.
  • On exiting the program, you need to wait for the all the files to finish downloading

So here's my solution, revised:

Initializes the Semaphore with a large enough counter so you never hit the maximum (it could be simply 100 or just 10 depending on your situation):

var maxDownloads = 1000;
_semaphore = new Semaphore(0, maxDownloads);

Then on each downloads, begins with WaitOne() before starting the download so that in the event of program exiting, no downloads can start.

if (_semaphore.WaitOne())
    /* proceeds with downloads */
else
    /* we're terminating */

Then on download completion, release one counter (if we had acquired one):

finally { _semaphore.Release(1); }

And then on the "Exit" event, consume up all the counters on the Semaphore:

for (var i = 0; i < maxDownloads; i++)
    _semaphore.WaitOne();

// all downloads are finished by this point.

...

chakrit
I'm afraid I don't quite understand. What if I don't know the max number of downloads? And if I put `_semahpore.WaitOne();` *before* I start the download, and the initial count is 0, won't it just wait indefinitely?
Mark
You should add this sentence to your question: "What if I don't know the max number of downloads?"
chakrit
@Mark You *should* know the number of async download operations undergoing concurrently in advance of the "blocking" event you want, otherwise if a new download can sprung up in the middle or during the blocking event, you need a whole different and complex system than just a simple semaphore or waithandles. You'll need a combination of them.
chakrit
How would a new download spring up during the blocking event? The blocking event occurs just before the program terminates...it waits for the files to finish, and then exits; no new downloads are started during this period. And even if it did, as long as the count didn't hit 0 before this new download started, there wouldn't be a problem, would there? This really makes no sense to me...
Mark
Ohhhh.....now I see what you're doing. Sorry that took me so long to get :D
Mark
@Mark I'm glad you got it... :)
chakrit
It's too bad sempahores don't have a `WaitMany(int)`... that loop just bugs the heck out of me, even if it is insignificantly costly :p
Mark
A: 

For each thread you start Interlock.Increment a counter. And for each callback on thread finish, Decrement it.

Then do a loop with a Thread.Sleep(10) or something until the count reaches zero.

Mikael Svenson
Real programs don't Sleep. Only demos do.
Henk Holterman
@Henk I think you meant: Real programs don't busy-wait. Only demos do.
chakrit
And I have to agree. Sloppy code on my part. I like both @chakrit's and @ssg's solutions. Btw, anyone have information on how the WaitOneNative inside the framwork is implemented?
Mikael Svenson
+4  A: 

Looks like System.Threading.WaitHandle.WaitAll might be a pretty good fit:

Waits for all the elements in the specified array to receive a signal.

Rytmis
This looks like a viable solution, although it seems a bit needlessly complex... pushing objects onto an array and setting/resetting them individually, when a simple counter ought to do...
Mark
+1  A: 

In .NET 4 there is a special type for that purpose CountdownEvent.

Or you can build similar thing yourself like this:

const int workItemsCount = 10;
// Set remaining work items count to initial work items count
int remainingWorkItems = workItemsCount;

using (var countDownEvent = new ManualResetEvent(false))
{
    for (int i = 0; i < workItemsCount; i++)
    {
        ThreadPool.QueueUserWorkItem(delegate
                                        {
                                            // Work item body
                                            // At the end signal event
                                            if (Interlocked.Decrement(ref remainingWorkItems) == 0)
                                                countDownEvent.Set();
                                        });
    }
    // Wait for all work items to complete
    countDownEvent.WaitOne();
}
Dzmitry Huba
Aw snap...that `CountdownEvent` looked so good until I tried it. It doesn't let you start the count at 0 and increment it because it's already signaled.
Mark
+3  A: 

I'd like to derive my answer from @Mikael's:

For each thread you start Interlocked.Increment a counter. And for each callback on thread finish, Decrement it.

If the counter reaches to zero when you decrement it, signal a NoMoreDownloads event.

private static int activeDownloadCount = 0;
private static ManualResetEvent noMoreDownloads = new ManualResetEvent(false);
public static ManualResetEvent NoMoreDownloads { get; }

void ThreadStart()
{
   Interlocked.Increment(activeDownloadCount);
   // ... rest of initialization code
}

void ThreadFinish()
{
   if(Interlocked.Decrement(activeDownloadCount) == 0)
   {
     NoMoreDownloads.Set();
   }
}

Here is the code that handles "no more downloads":

// runs in a separate thread 
// doesn't block anything
// doesn't consume CPU time.
void WaitForNoMoreDownloads(object stateInfo)
{
  DownloadThread.NoMoreDownloads.WaitOne();
  ShutdownApplication(); // can simply be an Application.Exit()
}

// register the handler 
ThreadPool.QueueWorkItem(new WaitCallback(WaitForNoMoreDownloads));

You have to note that this is not "multithread-perfect". For instance new downloads can start while you're shutting down the application or even before you signal the event. So this is the basic idea. You have to make sure proper locking is in place to make it perfect.

You can even put your shutdown code in ThreadFinish() if there are no gotchas there.

ssg
+7  A: 

Check out the CountdownLatch class in this magazine article.

Hans Passant
A fairly awesome article from start to finish.
spender
@spender - You'd hope so from Joe Duffy!
Greg Beech
That `BoundedBuffer` looks quite useful, but the `CountdownLatch` wasn't quite what I was looking for. I want to be able to Increment it as well (which should clear the signal).
Mark