Async and cancellation support for wait handles

The .NET framework comes with a number of low-level synchronization primitives. The most commonly used are collectively known as “wait handles”, and inherit the WaitHandle class: Semaphore, Mutex, AutoResetEvent and ManualResetEvent. These classes have been there since at least .NET 2.0 (1.1 for some of them), but they haven’t evolved much since they were introduced, which means they don’t support common features that were introduced later. In particular, they don’t provide support for waiting asynchronously, nor do they support cancelling the wait. Fortunately, it’s actually quite easy to add these features via extension methods.


Let’s start with the easiest one: cancellation. There are cases where it would be useful to pass a CancellationToken to WaitHandle.WaitOne, but none of the overloads supports it. Note that some more recent variants of the synchronization primitives, such as SemaphoreSlim and ManualResetEventSlim, do support cancellation; however, they’re not necessarily suitable for all use cases, because they’re designed for when the wait times are expected to be very short.

A CancellationToken exposes a WaitHandle, which is signaled when cancellation is requested. We can take advantage of this to implement a cancellable wait on a wait handle:

    public static bool WaitOne(this WaitHandle handle, int millisecondsTimeout, CancellationToken cancellationToken)
        int n = WaitHandle.WaitAny(new[] { handle, cancellationToken.WaitHandle }, millisecondsTimeout);
        switch (n)
            case WaitHandle.WaitTimeout:
                return false;
            case 0:
                return true;
                return false; // never reached

We use WaitHandle.WaitAny to wait for either the original wait handle or the cancellation token’s wait handle to be signaled. WaitAny returns the index of the first wait handle that was signaled, or WaitHandle.WaitTimeout if a timeout occurred before any of the wait handles was signaled. So we can have 3 possible outcomes:

  • a timeout occurred: we return false (like the standard WaitOne method);
  • the original wait handle is signaled first: we return true (like the standard WaitOne method);
  • the cancellation token’s wait handle is signaled first: we throw an OperationCancelledException.

    For completeness, let’s add some overloads for common use cases:

        public static bool WaitOne(this WaitHandle handle, TimeSpan timeout, CancellationToken cancellationToken)
            return handle.WaitOne((int)timeout.TotalMilliseconds, cancellationToken);
        public static bool WaitOne(this WaitHandle handle, CancellationToken cancellationToken)
            return handle.WaitOne(Timeout.Infinite, cancellationToken);

    And that’s it, we now have a cancellable WaitOne method!

    Asynchronous wait

    Now, what about asynchronous wait? That’s a bit harder. What we want here is a WaitOneAsync method that returns a Task<bool> (and since we’re at it, we might as well include cancellation support). The typical approach to create a Task wrapper for a non-task-based asynchronous operation is to use a TaskCompletionSource<T>, so that’s what we’ll do. When the wait handle is signaled, we’ll set the task’s result to true; if a timeout occurs, we’ll set it to false; and if the cancellation token is signaled, we’ll mark the task as cancelled.

    I struggled a bit to find a way to execute a delegate when a wait handle is signaled, but I eventually found the ThreadPool.RegisterWaitForSingleObject method, which exists for this exact purpose. I’m not sure why it’s in the ThreadPool class; I think it would have made more sense to put it in the WaitHandle class, but I assume there’s a good reason.

    So here’s what we’ll do:

  • create a TaskCompletionSource<bool>;
  • register a delegate to set the result to true when the wait handle is signaled, or false if a timeout occurs, using ThreadPool.RegisterWaitForSingleObject;
  • register a delegate to mark the task as cancelled when the cancellation token is signaled, using CancellationToken.Register;
  • unregister both delegates after the task completes

    Here’s the implementation:

        public static async Task<bool> WaitOneAsync(this WaitHandle handle, int millisecondsTimeout, CancellationToken cancellationToken)
            RegisteredWaitHandle registeredHandle = null;
            CancellationTokenRegistration tokenRegistration = default(CancellationTokenRegistration);
                var tcs = new TaskCompletionSource<bool>();
                registeredHandle = ThreadPool.RegisterWaitForSingleObject(
                    (state, timedOut) => ((TaskCompletionSource<bool>)state).TrySetResult(!timedOut),
                tokenRegistration = cancellationToken.Register(
                    state => ((TaskCompletionSource<bool>)state).TrySetCanceled(),
                return await tcs.Task;
                if (registeredHandle != null)
        public static Task<bool> WaitOneAsync(this WaitHandle handle, TimeSpan timeout, CancellationToken cancellationToken)
            return handle.WaitOneAsync((int)timeout.TotalMilliseconds, cancellationToken);
        public static Task<bool> WaitOneAsync(this WaitHandle handle, CancellationToken cancellationToken)
            return handle.WaitOneAsync(Timeout.Infinite, cancellationToken);

    Note that the lambda expressions could have used the tcs variable directly; this would make the code more readable, but it would cause a closure to be created, so as a small performance optimization, tcs is passed as the state parameter.

    We can now use the WaitOneAsync method like this:

    var mre = new ManualResetEvent(false);
    if (await mre.WaitOneAsync(2000, cancellationToken))

    Important note: this method will not work for a Mutex, because it relies on RegisterWaitForSingleObject, which is documented to work only on wait handles other than Mutex.


    We saw that with just a few extension methods, we made the standard synchronization primitives much more usable in typical modern code involving asynchrony and cancellation. However, I can hardly finish this post without mentioning Stephen Cleary’s AsyncEx library; it’s a rich toolbox which offers async-friendly versions of most standard primitives, some of which will let you achieve the same result as the code above. I encourage you to have a look at it, there’s plenty of good stuff in it.

  • 12 thoughts on “Async and cancellation support for wait handles”

      1. Thanks Stephen!

        Yes, I’ve seen it. I wasn’t too confident about my usage of RegisterWaitForSingleObject, but then I saw that you had used the same approach, which was comforting ;). I borrowed a few ideas from your method to improve my own method (e.g. passing the TCS via the state parameter to avoid the closure).

        I see that you used ContinueWith to dispose the registrations, do you know if it’s significantly more efficient than using await like I did?

      1. Hi Mike,

        Good point. The doc does mention it, but I didn’t read it carefully enough… I’ll update the code. Thanks!

    1. Halfway there. I wouldn’t recommend using this technique in this form. The methods shouldn’t return a task but a disposable registration, exposing the task as a property. Otherwise there is a great danger of exhausting resources by creating tasks which might not be awaited on to e.g. in Task.WhenAny

      1. Hi MoonStorm,

        Returning a task is the whole point of these methods, in order to be able to await asynchronously. And what you say about tasks not being awaited is true for any method that return a task 😉

    2. Your WaitOneAsync extension method doesn’t need to be (and shouldn’t be) ‘async’. You can just ‘return tcs.Task’ directly from a normal Task-returning method and avoid turning the function into an async state machine.

    3. Hmm, nevermind on my last comment? Seems like you want to dispose the registration after awaiting the returned Task. In that case, you can still return the `tcs.Task’, but attach a simple continuation to do the cleanup using `ContinueWith`, which ends up being exactly what the state machine would give you…

    4. I have just run into race condition when cancellationToken.Register throws exception because CTS was disposed. I think it would be good to to check if cancellation is requested before calling Register method, since usually CTS is cancelled and then disposed, what do you recommend?

      1. Hi @Tomasz,

        I don’t think checking if cancellation is requested would help. If the CTS is disposed before being canceled, the token still has IsCancellationRequested = false and CanBeCanceled = true, so you can’t detect if the CTS is disposed (other than by catching the exception). I guess the method could return early if the token is already canceled, but it can’t do anything about an already disposed CTS…

    Leave a Reply

    Your email address will not be published. Required fields are marked *