Building Async Coordination Primitives, Part 1: AsyncManualResetEvent

Building Async Coordination Primitives, Part 1: AsyncManualResetEvent

Rate This
  • Comments 7

The Task-based Async Pattern (TAP) isn’t just about asynchronous operations that you initiate and then asynchronously wait for to complete.  More generally, tasks can be used to represent all sorts of happenings, enabling you to await for any matter of condition to occur.  We can even use Tasks to build simple coordination primitives like their synchronous counterparts but that allow the waiting to be done asynchronously.

One of the more basic coordination primitives is an event, and there are a few of these in the .NET Framework. ManualResetEvent and AutoResetEvent wrap their Win32 counterparts, and then more recently .NET 4 saw the addition of ManualResetEventSlim, which is a lighter-weight version of ManualResetEvent.  An event is something that one party can wait on for another party to signal.  In the case of a manual-reset event, the event remains signaled after it’s been set and until it’s explicitly reset; until it is reset, all waits on the event succeed.

TaskCompletionSource<TResult> is itself a form of an event, just one without a reset. It starts in the non-signaled state; its Task hasn’t been completed, and thus all waits on the Task won’t complete until the Task is completed.  Then the {Try}Set* methods act as a signal, moving the Task into the completed state, such that all waits complete.  Thus, we can easily build an AsyncManualResetEvent on top of TaskCompletionSource<TResult>; the only behavior we’re really missing is the “reset” capability, which we’ll provide by swapping in a new TaskCompletionSource<TResult> instance.

Here’s the shape of the type we’re aiming to build:

public class AsyncManualResetEvent
{
    public Task WaitAsync();
    public void Set();
    public void Reset();
}

WaitAsync and Set are both easy.  Wrapping a TaskCompletionSource<bool>, Set will complete the TaskCompletionSource<bool> with TrySetResult, and WaitAsync will return the completion source’s Task:

public class AsyncManualResetEvent
{
    private volatile TaskCompletionSource<bool> m_tcs = new TaskCompletionSource<bool>();

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

    public void Set() { m_tcs.TrySetResult(true); }

    …
}

That leaves just the Reset method.  Our goal for Reset is to make the Tasks returned from subsequent calls to WaitAsync not completed, and since Tasks never transition from completed to not-completed, we need to swap in a new TaskCompletionSource<bool>.  In doing so, though, we need to make sure that, if multiple threads are calling Reset, Set, and WaitAsync concurrently, no Tasks returned from WaitAsync are orphaned (meaning that we wouldn’t want someone to call WaitAsync and get back a Task that won’t be completed the next time someone calls Set).  To achieve that, we’ll make sure to only swap in a new Task if the current one is already completed, and we’ll make sure that we do the swap atomically. (There are of course other policies that would be valid here; this is simply the one I’ve chosen for this particular example.)

public class AsyncManualResetEvent
{
    private volatile TaskCompletionSource<bool> m_tcs = new TaskCompletionSource<bool>();

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

    public void Set() { m_tcs.TrySetResult(true); }

    public void Reset()
    {
        while (true)
        {
            var tcs = m_tcs;
            if (!tcs.Task.IsCompleted ||
                Interlocked.CompareExchange(ref m_tcs, new TaskCompletionSource<bool>(), tcs) == tcs)
                return;
        }
    }
}

With that, our type is done.  However, there is one more potentially important behavior to keep in mind.  In previous posts, I’ve talked about continuations and how they can be made to execute synchronously, meaning that the continuation will execute as part of the task’s completion, synchronously on the same thread that’s completed the task.  In the case of TaskCompletionSource<TResult>, that means that synchronous continuations can happen as part of a call to {Try}Set*, which means in our AsyncManualResetEvent example, those continuations could execute as part of the Set method.  Depending on your needs (and whether callers of Set may be ok with a potentially longer-running Set call as all synchronous continuations execute), this may or may not be what you want.  If you don’t want this to happen, there are a few alternative approaches.  One approach is to run the completion asynchronously, having the call to set block until the task being completed is actually finished (not including the task’s synchronous continuations, just the task itself), e.g.

public void Set()
{
    var tcs = m_tcs;

    Task.Factory.StartNew(s => ((TaskCompletionSource<bool>)s).TrySetResult(true),
        tcs, CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default);
    tcs.Task.Wait();
}

There are of course other possible approaches, and what you do depends on your needs.

Next time, we’ll take a look at implementing an async auto-reset event.

Leave a Comment
  • Please add 6 and 7 and type the answer here:
  • Post
  • Stephen,

    Great content, as always.

    May I propose using locks to protect the TaskCompletionSource member field?  It might better serve the general audience, without muddying up the issue with volatile and Interlocked.  Your approach feels like a solution that was optimized for performance.

    It would also be instructional to see how such code is tested.

  • Hi Gregory-

    I'm glad you like the content.  And thanks for the suggestion; I'll keep that in mind for future posts.

  • Hi Stephen,

    Thanks for these inspiring articles!

    I got a question about the Reset() part of the sample code:

    why do we need the "while" statement?

    According to the code, the only chance to repeat the process in the "while" block is when "tcs" is not equal to "m_tcs", and that means there is another thread called "Reset" and assigned a new instance to "m_tcs". In such case,  is there any problem if we just return the "Reset" call and not do the process in the "while" block again?

  • Hi Upsilon-

    I'm glad you enjoyed the series. Regarding Reset, it just depends on how aggressive you want the method to be, e.g. if you're ok with Reset being less aggressive (which functionally should be fine), you could remove the while, but you still do need the interlocked.

  • Stephen,

    Do you suggest using AsyncManualResetEvent and other synchronization context (AsyncSemaphore etc..)   inside async/await method? Is AsyncManualResetEvent going to serve the same purpose as ManualResetEven in synchronous method?

  • @Saurabh: If you're trying to do work asynchronously, generally you don't want to be blocking threads for any non-trivial amount of time, and as such if you do need to do such coordination between activities, it's usually best to use asynchronous primitives rather than synchronous ones.

  • If this is as easy as above, why this hasn't landed to BCL yet?

Page 1 of 1 (7 items)