51

What would be the async (awaitable) equivalent of AutoResetEvent?

If in the classic thread synchronization we would use something like this:

    AutoResetEvent signal = new AutoResetEvent(false);

    void Thread1Proc()
    {
        //do some stuff
        //..
        //..

        signal.WaitOne(); //wait for an outer thread to signal we are good to continue

        //do some more stuff
        //..
        //..
    }

    void Thread2Proc()
    {
        //do some stuff
        //..
        //..

        signal.Set(); //signal the other thread it's good to go

        //do some more stuff
        //..
        //..
    }

I was hoping that in the new async way of doing things, something like this would come to be:

SomeAsyncAutoResetEvent asyncSignal = new SomeAsyncAutoResetEvent();

async void Task1Proc()
{
    //do some stuff
    //..
    //..

    await asyncSignal.WaitOne(); //wait for an outer thread to signal we are good to continue

    //do some more stuff
    //..
    //..
}

async void Task2Proc()
{
    //do some stuff
    //..
    //..

    asyncSignal.Set(); //signal the other thread it's good to go

    //do some more stuff
    //..
    //..
}

I've seen other custom made solutions, but what I've managed to get my hands on, at some point in time, still involves locking a thread. I don't want this just for the sake of using the new await syntax. I'm looking for a true awaitable signaling mechanism which does not lock any thread.

Is it something I'm missing in the Task Parallel Library?

EDIT: Just to make clear: SomeAsyncAutoResetEvent is an entirely made up class name used as a placeholder in my example.

Mihai Caracostea
  • 8,336
  • 4
  • 27
  • 46

11 Answers11

34

If you want to build your own, Stephen Toub has the definitive blog post on the subject.

If you want to use one that's already written, I have one in my AsyncEx library. AFAIK, there's no other option as of the time of this writing.

divega
  • 6,320
  • 1
  • 31
  • 31
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 5
    Why wouldn't a `new SemaphoreSlim(1)` work, `WaitOne()` is `WaitAsync()` and `Set()` becomes `Release()` – Scott Chamberlain Nov 02 '16 at 21:40
  • 2
    AREs and Semaphores are very similar (though usually used differently). The semantic difference comes in if the primitive is signalled when it is already set. – Stephen Cleary Nov 03 '16 at 01:01
  • anything wrong with `await Task.Run(() => loginWaiter.WaitOne(TimeSpan.FromSeconds(75)));` – Ashley Jackson Jan 25 '19 at 15:51
  • 1
    @AshleyJackson: That approach does use another thread. Some synchronization primitives do not allow this (e.g., `Mutex`, `Monitor`), but since this is an `AutoResetEvent`, it should work. – Stephen Cleary Jan 25 '19 at 17:44
  • 7
    I think those who are named "Stephen" are born for asynchronous anything. – M.kazem Akhgary Jan 27 '19 at 07:12
  • 8
    Stephen Toubs post seems to have been moved [here](https://devblogs.microsoft.com/pfxteam/building-async-coordination-primitives-part-2-asyncautoresetevent/) – Klepto Aug 06 '20 at 08:36
23

Here's the source for Stephen Toub's AsyncAutoResetEvent, in case his blog goes offline.

public class AsyncAutoResetEvent
{
    private static readonly Task s_completed = Task.FromResult(true);
    private readonly Queue<TaskCompletionSource<bool>> m_waits = new Queue<TaskCompletionSource<bool>>();
    private bool m_signaled;

    public Task WaitAsync()
    {
        lock (m_waits)
        {
            if (m_signaled)
            {
                m_signaled = false;
                return s_completed;
            }
            else
            {
                var tcs = new TaskCompletionSource<bool>();
                m_waits.Enqueue(tcs);
                return tcs.Task;
            }
        }
    }

    public void Set()
    {
        TaskCompletionSource<bool> toRelease = null;

        lock (m_waits)
        {
            if (m_waits.Count > 0)
                toRelease = m_waits.Dequeue();
            else if (!m_signaled)
                m_signaled = true;
        }

        toRelease?.SetResult(true);
    }
}
KFL
  • 17,162
  • 17
  • 65
  • 89
Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
  • Why can you use regular lock in awaitable code? Can't the same task continue as a different thread here and go around the lock? – user1713059 Jul 11 '19 at 14:52
  • 3
    @user1713059 note that `WaitAsync` isn't actually an `async` method. That means it doesn't yield control midway through processing. Instead, it obtains a `Task` from the `TaskCompletionSource` and returns it before releasing the lock. – Drew Noakes Jul 17 '19 at 01:23
  • Ah sure, so even if I do "await WaitAsync()" it is sure that the whole method gets executed by the same thread, because it's not actually async - is that right? The "Async" method suffix led me astray, but from what I see it's used in methods without the "async" keyword too. – user1713059 Jul 18 '19 at 20:22
  • 1
    It's still an asynchronous method because it returns a task which may not be completed by the time the method returns. However the method is not `async`, which means the method won't yield at some point within its body while it `await`s some other `Task`'s completion. It's convention for methods that return `Task` (or `Task`) to have an `Async` suffix. – Drew Noakes Jul 21 '19 at 10:59
  • 1
    With respect to your original comment, the lock is released _before_ the `Task` is returned to the caller, so there's no way for that caller to get around the lock. – Drew Noakes Jul 21 '19 at 11:01
18

I think there is good example on MSDN: https://msdn.microsoft.com/en-us/library/hh873178%28v=vs.110%29.aspx#WHToTap

public static Task WaitOneAsync(this WaitHandle waitHandle)
{
    if (waitHandle == null) 
        throw new ArgumentNullException("waitHandle");

    var tcs = new TaskCompletionSource<bool>();
    var rwh = ThreadPool.RegisterWaitForSingleObject(waitHandle, 
        delegate { tcs.TrySetResult(true); }, null, -1, true);
    var t = tcs.Task;
    t.ContinueWith( (antecedent) => rwh.Unregister(null));
    return t;
}
Oleg Gordeev
  • 554
  • 4
  • 6
  • Definitely the best answer. – Felix K. May 05 '21 at 12:46
  • Note that this only works correctly on ManualResetEvent not AutoResetEvent. On AutoResetEvent you need to WaitOne the thing inside the delegate; else the event is still signalled next time somebody calls WaitOne on it. – Joshua Mar 16 '22 at 16:42
9

Here is a version I cooked up which allows you to specify a timeout. It is derived from Stephen Toub's solution. We currently use this in production workloads.

public class AsyncAutoResetEvent
{
    readonly LinkedList<TaskCompletionSource<bool>> waiters = 
        new LinkedList<TaskCompletionSource<bool>>();

    bool isSignaled;

    public AsyncAutoResetEvent(bool signaled)
    {
        this.isSignaled = signaled;
    }

    public Task<bool> WaitAsync(TimeSpan timeout)
    {
        return this.WaitAsync(timeout, CancellationToken.None);
    }

    public async Task<bool> WaitAsync(TimeSpan timeout, CancellationToken cancellationToken)
    {
        TaskCompletionSource<bool> tcs;

        lock (this.waiters)
        {
            if (this.isSignaled)
            {
                this.isSignaled = false;
                return true;
            }
            else if (timeout == TimeSpan.Zero)
            {
                return this.isSignaled;
            }
            else
            {
                tcs = new TaskCompletionSource<bool>();
                this.waiters.AddLast(tcs);
            }
        }

        Task winner = await Task.WhenAny(tcs.Task, Task.Delay(timeout, cancellationToken));
        if (winner == tcs.Task)
        {
            // The task was signaled.
            return true;
        }
        else
        {
            // We timed-out; remove our reference to the task.
            // This is an O(n) operation since waiters is a LinkedList<T>.
            lock (this.waiters)
            {
                bool removed = this.waiters.Remove(tcs);
                Debug.Assert(removed);
                return false;
            }
        }
    }

    public void Set()
    {
        lock (this.waiters)
        {
            if (this.waiters.Count > 0)
            {
                // Signal the first task in the waiters list. This must be done on a new
                // thread to avoid stack-dives and situations where we try to complete the
                // same result multiple times.
                TaskCompletionSource<bool> tcs = this.waiters.First.Value;
                Task.Run(() => tcs.SetResult(true));
                this.waiters.RemoveFirst();
            }
            else if (!this.isSignaled)
            {
                // No tasks are pending
                this.isSignaled = true;
            }
        }
    }

    public override string ToString()
    {
        return $"Signaled: {this.isSignaled.ToString()}, Waiters: {this.waiters.Count.ToString()}";
    }
}
Chris Gillum
  • 14,526
  • 5
  • 48
  • 61
  • 1
    I think this.waiters should be lock'ed at the in the Remove(tcs) manipulation path? – HelloSam May 09 '17 at 07:37
  • @HelloSam I think you are right! Fixed. Thanks for pointing this out. – Chris Gillum May 11 '17 at 18:33
  • I don't have a lot of time to debug this, but, be forewarned: i am getting dead-lock using this. When a new thread calls event.Set(), it hangs on `toRelease.SetResult(true);` – Andy Dec 11 '18 at 04:37
  • 1
    @Andy thanks for the comment. There is an additional fix I made since I originally posted this which I suspect addresses your deadlock (in my case, it was a StackOverflowException). The fix was to wrap the `SetResult(true)` call in a `Task.Run(...)`. – Chris Gillum Dec 12 '18 at 20:51
  • Am I mistaken or is it not auto-resetting where it returns true after `if (winner == tcs.Task)`? – Hugh Jeffner Oct 07 '21 at 21:31
  • Never mind, I didn't understand how it works. Turns out if there are tasks waiting it never bothers setting `isSignaled` – Hugh Jeffner Oct 08 '21 at 13:06
3

I was also looking for an AsyncAutoResetEvent class and it seems there is now one available in namespace Microsoft.VisualStudio.Threading

// Summary:
//     An asynchronous implementation of an AutoResetEvent.
[DebuggerDisplay("Signaled: {signaled}")]
public class AsyncAutoResetEvent
IvoTops
  • 3,463
  • 17
  • 18
  • Here it is: https://github.com/microsoft/vs-threading/blob/main/src/Microsoft.VisualStudio.Threading/AsyncAutoResetEvent.cs – Salar Jan 16 '23 at 18:54
2

It also works, but this way may fade the purpose of using async and await.

AutoResetEvent asyncSignal = new AutoResetEvent();

async void Task1Proc()
{
    //do some stuff
    //..
    //..

    await Task.Run(() => asyncSignal.WaitOne()); //wait for an outer thread to signal we are good to continue

    //do some more stuff
    //..
    //..
}
Hyunjik Bae
  • 2,721
  • 2
  • 22
  • 32
  • Why is this considered bad? – Yarek T Feb 01 '21 at 11:59
  • @YarekT I remembered the reason at the time I wrote this answer months ago, but not now. I don't think this is bad, though there are more than one context switching (by WaitOne() and by await keyword) performance issue in this. – Hyunjik Bae Feb 02 '21 at 22:02
  • 1
    No worries. I've been recently looking more into Tasks in C#. From what I can gather its bad because it wastes a thread by creating one, then immediately making it blocked by the wait. I've seen a a few solutions floating around that avoid this by somehow using a timer, but they all seem very complicated. Anyway, heres an upvote – Yarek T Feb 12 '21 at 09:05
2

This could be much simpler implementation with SemaphoreSlim, I haven't tested this in production yet but it should work fine.

public class AsyncAutoResetEvent : IDisposable
{
    private readonly SemaphoreSlim _waiters;

    public AsyncAutoResetEvent(bool initialState)
        => _waiters = new SemaphoreSlim(initialState ? 1 : 0, 1);

    public Task<bool> WaitOneAsync(TimeSpan timeout, CancellationToken cancellationToken = default)
        => _waiters.WaitAsync(timeout, cancellationToken);

    public Task WaitOneAsync(CancellationToken cancellationToken = default)
        => _waiters.WaitAsync(cancellationToken);

    public void Set()
    {
        lock (_waiters)
        {
            if (_waiters.CurrentCount == 0)
                _waiters.Release();
        }
    }

    public override string ToString()
        => $"Signaled: {_waiters.CurrentCount != 0}"; 

    private bool _disposed;
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                _waiters.Dispose();
            }

            _disposed = true;
        }
    }

    public void Dispose()
    {
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
}
Faraz M. Khan
  • 159
  • 1
  • 4
  • I have add this in Flettu Lib (just back lib for such tasks): https://github.com/mysteryjeans/Flettu/blob/master/src/Flettu/Lock/AsyncAutoResetEvent.cs – Faraz M. Khan Jan 12 '23 at 11:02
1

Install the Microsoft.VisualStudio.Threading package (i did it using Nuget), then you will be able to use the AsyncAutoResetEvent class, which by its own documentation states:

An asynchronous implementation of an AutoResetEvent.

docs: https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.threading.asyncautoresetevent

Vitox
  • 3,852
  • 29
  • 30
0

I extended the example from MSDN provided by Oleg Gordeev with an optional Timeout (ms):

public static Task WaitOneAsync(this WaitHandle waitHandle, double timeout = 0)
        {
            if (waitHandle == null) throw new ArgumentNullException("waitHandle");

            var tcs = new TaskCompletionSource<bool>();

            if (timeout > 0) 
            {
                var timer = new System.Timers.Timer(timeout) 
                { Enabled = true, AutoReset = false };

                ElapsedEventHandler del = default;
                del = delegate (object x, System.Timers.ElapsedEventArgs y)
                {
                    tcs.TrySetResult(true);
                    timer.Elapsed -= del; 
                    timer.Dispose();
                };

                timer.Elapsed += del;
            }
        
            var rwh = ThreadPool.RegisterWaitForSingleObject(waitHandle,
                      delegate { tcs.TrySetResult(true); },
                      null, -1, true);

            var t = tcs.Task;
            t.ContinueWith((antecedent) => rwh.Unregister(null));

            return t;
        }
-1

Here is my version of one-time event can be await by multiple threads. It internally relies on BoundedChannel.

public class AsyncOneTimeEvent<T>
{
    private T Result { get; set; }

    private readonly Channel<bool> _channel = Channel.CreateBounded<bool>(new BoundedChannelOptions(1)
    {
        SingleReader = false,
        SingleWriter = true,
        FullMode = BoundedChannelFullMode.DropWrite,
    });

    public async Task<T> GetResult()
    {
        await _channel.Reader.WaitToReadAsync().ConfigureAwait(false);

        return this.Result;
    }

    public void SetResult(T result)
    {
        this.Result = result;
        _channel.Writer.Complete();
    }

    public void SetError(Exception ex)
    {
        _channel.Writer.Complete(ex);
    }
}
Mr.Wang from Next Door
  • 13,670
  • 12
  • 64
  • 97
  • Using a `Channel` as a substitute of a `TaskCompletionSource` seems like a clever idea. But is also unnecessary, and the implementation seems susceptible to visibility problems. I am not sure that all threads will "see" the latest value of the non-volatile `private T Result` field in all cases. – Theodor Zoulias Jan 06 '21 at 13:30
  • Example: Thread A enters the `GetResult()` method, reads the value of `Result` in an out-of-order fashion, and then gets suspended by the OS. Thread B enters and exits the `SetResult` method. Thread A resumes, executes synchronously the `await _channel.Reader.WaitToReadAsync()` line, and returns a `Task` having `default(T)` as its value. Is this scenario impossible based on the C# ECMA-334 specification? I have no idea! – Theodor Zoulias Jan 06 '21 at 13:42
  • @TheodorZoulias certainly it works, you can try it online : https://dotnetfiddle.net/uyQRG1 – Mr.Wang from Next Door Jan 07 '21 at 11:24
  • I am sure that it works. I am not sure that it is guaranteed to work correctly on all CPU architectures. Visibility problems are notoriously difficult to debug. You can read [this article](https://learn.microsoft.com/en-us/archive/msdn-magazine/2012/december/csharp-the-csharp-memory-model-in-theory-and-practice) by Igor Ostrovsky to get an idea why. – Theodor Zoulias Jan 07 '21 at 14:42
-1

Here's my COMPLETE implementation using SemaphoreSlim, using all SemaphoreSlim.WaitAsync overrides:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

/// <summary>
/// Represents an event that, when signaled, resets automatically after releasing a single waiting task.
/// </summary>
public sealed class AutoResetEventAsync : IDisposable {

    /// <summary>
    /// Waits asynchronously until a signal is received.
    /// </summary>
    /// <returns>Task completed when the event is signaled.</returns>
    public async ValueTask WaitAsync() {
        if (CheckSignaled()) return;
        SemaphoreSlim s;
        lock (Q) Q.Enqueue(s = new(0, 1));
        await s.WaitAsync();
        lock (Q) if (Q.Count > 0 && Q.Peek() == s) Q.Dequeue().Dispose();
    }

    /// <summary>
    /// Waits asynchronously until a signal is received or the time runs out.
    /// </summary>
    /// <param name="millisecondsTimeout">The number of milliseconds to wait, <see cref="System.Threading.Timeout.Infinite"/>
    /// (-1) to wait indefinitely, or zero to return immediately.</param>
    /// <returns>Task completed when the event is signaled or the time runs out.</returns>
    public async ValueTask WaitAsync(int millisecondsTimeout) {
        if (CheckSignaled()) return;
        SemaphoreSlim s;
        lock (Q) Q.Enqueue(s = new(0, 1));
        await s.WaitAsync(millisecondsTimeout);
        lock (Q) if (Q.Count > 0 && Q.Peek() == s) Q.Dequeue().Dispose();
    }

    /// <summary>
    /// Waits asynchronously until a signal is received, the time runs out or the token is cancelled.
    /// </summary>
    /// <param name="millisecondsTimeout">The number of milliseconds to wait, <see cref="System.Threading.Timeout.Infinite"/>
    /// (-1) to wait indefinitely, or zero to return immediately.</param>
    /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> to observe.</param>
    /// <returns>Task completed when the event is signaled, the time runs out or the token is cancelled.</returns>
    public async ValueTask WaitAsync(int millisecondsTimeout, CancellationToken cancellationToken) {
        if (CheckSignaled()) return;
        SemaphoreSlim s;
        lock (Q) Q.Enqueue(s = new(0, 1));
        try {
            await s.WaitAsync(millisecondsTimeout, cancellationToken);
        }
        finally {
            lock (Q) if (Q.Count > 0 && Q.Peek() == s) Q.Dequeue().Dispose();
        }
    }

    /// <summary>
    /// Waits asynchronously until a signal is received or the token is cancelled.
    /// </summary>
    /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> to observe.</param>
    /// <returns>Task completed when the event is signaled or the token is cancelled.</returns>
    public async ValueTask WaitAsync(CancellationToken cancellationToken) {
        if (CheckSignaled()) return;
        SemaphoreSlim s;
        lock (Q) Q.Enqueue(s = new(0, 1));
        try {
            await s.WaitAsync(cancellationToken);
        }
        finally {
            lock (Q) if (Q.Count > 0 && Q.Peek() == s) Q.Dequeue().Dispose();
        }
    }

    /// <summary>
    /// Waits asynchronously until a signal is received or the time runs out.
    /// </summary>
    /// <param name="timeout">A <see cref="System.TimeSpan"/> that represents the number of milliseconds to wait,
    /// a <see cref="System.TimeSpan"/> that represents -1 milliseconds to wait indefinitely, or a System.TimeSpan
    /// that represents 0 milliseconds to return immediately.</param>
    /// <returns>Task completed when the event is signaled or the time runs out.</returns>
    public async ValueTask WaitAsync(TimeSpan timeout) {
        if (CheckSignaled()) return;
        SemaphoreSlim s;
        lock (Q) Q.Enqueue(s = new(0, 1));
        await s.WaitAsync(timeout);
        lock (Q) if (Q.Count > 0 && Q.Peek() == s) Q.Dequeue().Dispose();
    }

    /// <summary>
    /// Waits asynchronously until a signal is received, the time runs out or the token is cancelled.
    /// </summary>
    /// <param name="timeout">A <see cref="System.TimeSpan"/> that represents the number of milliseconds to wait,
    /// a <see cref="System.TimeSpan"/> that represents -1 milliseconds to wait indefinitely, or a System.TimeSpan
    /// that represents 0 milliseconds to return immediately.</param>
    /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> to observe.</param>
    /// <returns>Task completed when the event is signaled, the time runs out or the token is cancelled.</returns>
    public async ValueTask WaitAsync(TimeSpan timeout, CancellationToken cancellationToken) {
        if (CheckSignaled()) return;
        SemaphoreSlim s;
        lock (Q) Q.Enqueue(s = new(0, 1));
        try {
            await s.WaitAsync(timeout, cancellationToken);
        }
        finally {
            lock (Q) if (Q.Count > 0 && Q.Peek() == s) Q.Dequeue().Dispose();
        }
    }

    /// <summary>
    /// Sets the state of the event to signaled, allowing one or more waiting tasks to proceed.
    /// </summary>
    public void Set() {
        SemaphoreSlim? toRelease = null;
        lock (Q) {
            if (Q.Count > 0) toRelease = Q.Dequeue();
            else if (!IsSignaled) IsSignaled = true;
        }
        toRelease?.Release();
    }

    /// <summary>
    /// Sets the state of the event to non nonsignaled, making the waiting tasks to wait.
    /// </summary>
    public void Reset() => IsSignaled = false;

    /// <summary>
    /// Disposes any semaphores left in the queue.
    /// </summary>
    public void Dispose() {
        lock (Q) {
            while (Q.Count > 0) Q.Dequeue().Dispose();
        }
    }

    /// <summary>
    /// Checks the <see cref="IsSignaled"/> state and resets it when it's signaled.
    /// </summary>
    /// <returns>True if the event was in signaled state.</returns>
    private bool CheckSignaled() {
        lock (Q) {
            if (IsSignaled) {
                IsSignaled = false;
                return true;
            }
            return false;
        }
    }

    private readonly Queue<SemaphoreSlim> Q = new();
    private volatile bool IsSignaled;

}

I used SemaphoreSlim because it gives the time out and cancellation token support "for free". It could be even better if I just modified the original .NET source code of SemaphoreSlim to behave like AutoResetEvent but nah, that's it. Let me know if you find any bugs.

Harry
  • 4,524
  • 4
  • 42
  • 81
  • Is the `AutoResetEventAsync` class thread-safe? If yes, what can happen if two threads call `WaitAsync()` at the same time? Isn't it possible that both will read the `IsSignaled` field as `true`, before any of them executes the `IsSignaled = false;` line? Also the `if (Q.Contains(s)) Q.Dequeue().Dispose();` line searches if the `s` exists in the queue, and then dequeues and disposes some other semaphore (most likely). Is this intentional? – Theodor Zoulias Sep 12 '21 at 17:07
  • @TheodorZoulias : Yes, because even if 2 threads can enter `WaitAsync` at the same time, they cannot pass the `Q` access at the same time. Notice that `Q` can be only accessed with a single thread. That makes the flow simple and direct. This also implies the internal await is accessible to a single thread only. Thus, it is impossible invalid semaphore is dequeued. The multiple tests I performed on this class haven't yet fail, but this doesn't prove it's valid. I think that the single tread access to Q does. – Harry Sep 15 '21 at 05:58
  • 1
    I am talking about this line: `if (IsSignaled) { IsSignaled = false; return; }`. This is not protected by a lock. The `IsSignaled` is not even a [`volatile`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/volatile) field. As for the `if (Q.Contains(s))`, if you are sure that the `s` can only be in the head of the queue, `if (Q.Peak() == s)` would be faster and more expressive regarding the intentions of the code. Btw what will happen if the `cancellationToken` is canceled and the `WaitAsync` throws? – Theodor Zoulias Sep 15 '21 at 06:28
  • You've found some interesting edge cases. I'll try to fix them and edit my example... BRB. – Harry Sep 15 '21 at 19:07
  • Fixed. I had to add `if (Q.Count > 0)` because `Peek()` throws when Q is empty, and it is empty in most common case the `Set()` is invoked. – Harry Sep 15 '21 at 19:39
  • Your implementation transitioned from certainly flawed to potentially correct. But after closer inspection one race condition still exists (and maybe more). Assuming an initial state `IsSignaled = false`, one thread invokes the `WaitAsync` and another invokes the `Set`. The expected behavior after both invocations is that the first thread will **not** be in a waiting state. Alas your implementation allows this to happen, in case the `Set` is invoked by the second thread just after the first thread has executed the `CheckSignaled()`, and just before executing the `Q.Enqueue(s = new(0, 1))`. – Theodor Zoulias Sep 15 '21 at 21:09
  • Btw I am not sure what the `Reset` method is supposed to do, but mutating a non-volatile field without synchronization opens the can of worms that is called [memory models](https://learn.microsoft.com/en-us/archive/msdn-magazine/2012/december/csharp-the-csharp-memory-model-in-theory-and-practice). Are you [an expert in coherency protocols](https://stackoverflow.com/a/66490395/11178549), and can guarantee that this is not a problem? If not, then neither am I, and so there is no chance that I would use your class (as is) in a production environment. – Theodor Zoulias Sep 15 '21 at 21:19
  • The `Reset` seems to be redundant here, so it goes. About the race condition - I give up. `WaitAsync` can't block `Set()`. So how could I possibly prevent that particular condition from happening? BTW, that would happen only if I invoke `Set()` parallelly with `WaitAsync()` that would be just asking for a race condition. In my code I use this class to proceed when I/O operation completes. This event is used for one client session, so this will never happen. The I/O operation will never be completed before `WaitAsync()` enters `Enqueue()` part. Even if it throws. – Harry Sep 16 '21 at 14:58
  • Harry since you realize that implementing a custom asynchronous `AutoResetEvent` is challenging, why are you trying to roll your own, and don't just use Stephen Cleary's [`AsyncAutoResetEvent`](https://github.com/StephenCleary/AsyncEx/wiki/AsyncAutoResetEvent)? – Theodor Zoulias Sep 16 '21 at 15:03
  • BTW, this class is not intended to be used to synchronize threads. Mixing manual thread synchronization with async pattern is not a good idea, because I think it's just extremely difficult to do correctly. That's probably why there is no `WaitAsync()` method in `AutoResetEvent` class in the first place. – Harry Sep 16 '21 at 15:04
  • Why? Because I needed a version that supports timeout and `CancellationToken`. I just needed it for my code, I use it and I just thought I could share it. – Harry Sep 16 '21 at 15:05
  • Hmm, Stephen Cleary's `AsyncAutoResetEvent` supports `CancellationToken`s, so the timeout functionality can be built on top of that. I just posted [here](https://gist.github.com/theodorzoulias/cc7f38d5423eee9f238f349605fc400a) an extension method that supports timeout and `CancellationToken` (optionally). Btw when you have an asynchronous implementation, implementing the synchronous functionality is trivial. Just call the asynchronous with `.GetAwaiter.GetResult()`. It's not very efficient, but gets the job done. – Theodor Zoulias Sep 16 '21 at 15:25
  • 1
    Thank you for the insight, I will use the better version with your extension then. Anyway, it was totally worth it as a learning experience. You're MVP. – Harry Sep 16 '21 at 15:28