Building Async Coordination Primitives, Part 3: AsyncCountdownEvent

In my last two posts, I discussed building AsyncManualResetEvent and AsyncAutoResetEvent coordination primitives.  In this post, I’ll build on that to create a simple AsyncCountdownEvent.

A countdown event is an event that will allow waiters to complete after receiving a particular number of signals.  The “countdown” comes from the common fork/join pattern in which it’s often utilized: a certain number of operations participate, and as they complete they signal the event, which counts down from the original number to 0.  When it gets to 0, it becomes set, and all waiters can complete.

The shape of our type will be as follows:

public class AsyncCountdownEvent
{
    public AsyncCountdownEvent(int initialCount);
    public Task WaitAsync();
    public void Signal();
}

At its core, a countdown event is really just a manual reset event and an integral count, so our AsyncCountdownEvent will have two members:

private readonly AsyncManualResetEvent m_amre = new AsyncManualResetEvent();
private int m_count;

The constructor of our type simply initializes m_count based on a supplied number of signals:

public AsyncCountdownEvent(int initialCount)
{
    if (initialCount <= 0) throw new ArgumentOutOfRangeException(“initialCount”);
    m_count = initialCount;
}

The WaitAsync method is then trivial, as it’ll just delegate to the corresponding method on the AsyncManualResetEvent:

public Task WaitAsync() { return m_amre.WaitAsync(); }

Finally, our Signal method will decrement m_count, and if that brings it to 0, we’ll Set the m_amre:

public void Signal()
{
    if (m_count <= 0)
        throw new InvalidOperationException();

    int newCount = Interlocked.Decrement(ref m_count);
    if (newCount == 0)
        m_amre.Set();
    else if (newCount < 0)
        throw new InvalidOperationException();
}

One common usage of a type like AsyncCountdownEvent is using it as a form of a barrier: all participants signal and then wait for all of the other participants to arrive.  Given that, we could also add a simple SignalAndWait method to implement this common pattern:

public Task SignalAndWait()
{
    Signal();
    return WaitAsync();
}

Next time, we’ll take a look at implementing an actual asynchronous barrier, one that can be used over and over by the participants to operate in lock step.