Building Async Coordination Primitives, Part 3: AsyncCountdownEvent

Building Async Coordination Primitives, Part 3: AsyncCountdownEvent

Rate This
  • Comments 1

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.

Leave a Comment
  • Please add 7 and 3 and type the answer here:
  • Post
  • Again: simple and clear explanation. Thanks very much, Stephen.

    One thing that seemed "wrong" to me is disallowing an AsyncManualResetEvent with an initial count of zero. Doing so is inconsistent with, for example, SemaphoreSlim and potentially makes client code more difficult. Imagine, for example, client code such that uses some internal collection to decide the initial count, and then returns the Task returned by WaitAsync to its clients. Such code would first need to check whether the collection is empty and return Task.FromResult(true) in that instance. It seems to me that AsyncManualResetEvent should special-case a zero initial count by signalling the wrapped AsyncManualResetEvent.

    But whatever, I am absolutely loving this series! (I know it's from a while back, but I somehow missed it).

Page 1 of 1 (1 items)