Awaitable AutoResetEvent
Solution 1
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.
Solution 2
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);
}
}
Solution 3
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;
}
Solution 4
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()}";
}
}
Related videos on Youtube
Mihai Caracostea
Updated on July 22, 2022Comments
-
Mihai Caracostea almost 2 years
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.
-
Dark Falcon over 8 yearsFor one-time-use, a
TaskCompletionSource
whose result is ignored by the awaiting task. -
Matthew Watson over 8 years
-
Mihai Caracostea over 8 years@MatthewWatson I see it uses a lock, which will block a thread from the thread pool. I was hoping for something not involving a blocked thread.
-
Dark Falcon over 8 yearsA lock doesn't necessarily mean a thread is blocked.
-
Mihai Caracostea over 8 years@DarkFalcon True. And in this case it might even not block any thread.
-
Mihai Caracostea over 8 years@MatthewWatson This extension method uses ThreadPool.RegisterWaitForSingleObject which "Registers a delegate that is waiting for a WaitHandle.". I understand from this bit that a thread from the pool will block on that waithandle. Am I wrong?
-
-
Scott Chamberlain over 7 yearsWhy wouldn't a
new SemaphoreSlim(1)
work,WaitOne()
isWaitAsync()
andSet()
becomesRelease()
-
Stephen Cleary over 7 yearsAREs and Semaphores are very similar (though usually used differently). The semantic difference comes in if the primitive is signalled when it is already set.
-
HelloSam almost 7 yearsI think this.waiters should be lock'ed at the in the Remove(tcs) manipulation path?
-
Chris Gillum almost 7 years@HelloSam I think you are right! Fixed. Thanks for pointing this out.
-
Andy over 5 yearsI 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);
-
Chris Gillum over 5 years@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 aTask.Run(...)
. -
Ashley Jackson over 5 yearsanything wrong with
await Task.Run(() => loginWaiter.WaitOne(TimeSpan.FromSeconds(75)));
-
Stephen Cleary over 5 years@AshleyJackson: That approach does use another thread. Some synchronization primitives do not allow this (e.g.,
Mutex
,Monitor
), but since this is anAutoResetEvent
, it should work. -
M.kazem Akhgary about 5 yearsI think those who are named "Stephen" are born for asynchronous anything.
-
user1713059 almost 5 yearsWhy can you use regular lock in awaitable code? Can't the same task continue as a different thread here and go around the lock?
-
Drew Noakes almost 5 years@user1713059 note that
WaitAsync
isn't actually anasync
method. That means it doesn't yield control midway through processing. Instead, it obtains aTask
from theTaskCompletionSource
and returns it before releasing the lock. -
user1713059 almost 5 yearsAh 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.
-
Drew Noakes almost 5 yearsIt'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 itawait
s some otherTask
's completion. It's convention for methods that returnTask
(orTask<T>
) to have anAsync
suffix. -
Drew Noakes almost 5 yearsWith 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. -
Klepto over 3 yearsStephen Toubs post seems to have been moved here
-
Theodor Zoulias over 3 yearsUsing a
Channel
as a substitute of aTaskCompletionSource
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-volatileprivate T Result
field in all cases. -
Theodor Zoulias over 3 yearsExample: Thread A enters the
GetResult()
method, reads the value ofResult
in an out-of-order fashion, and then gets suspended by the OS. Thread B enters and exits theSetResult
method. Thread A resumes, executes synchronously theawait _channel.Reader.WaitToReadAsync()
line, and returns aTask
havingdefault(T)
as its value. Is this scenario impossible based on the C# ECMA-334 specification? I have no idea! -
Mr.Wang from Next Door over 3 years@TheodorZoulias certainly it works, you can try it online : dotnetfiddle.net/uyQRG1
-
Theodor Zoulias over 3 yearsI 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 by Igor Ostrovsky to get an idea why.
-
Yarek T about 3 yearsWhy is this considered bad?
-
Test about 3 years@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.
-
Yarek T about 3 yearsNo 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
-
Felix K. almost 3 yearsDefinitely the best answer.
-
Theodor Zoulias over 2 yearsIs the
AutoResetEventAsync
class thread-safe? If yes, what can happen if two threads callWaitAsync()
at the same time? Isn't it possible that both will read theIsSignaled
field astrue
, before any of them executes theIsSignaled = false;
line? Also theif (Q.Contains(s)) Q.Dequeue().Dispose();
line searches if thes
exists in the queue, and then dequeues and disposes some other semaphore (most likely). Is this intentional? -
Harry over 2 years@TheodorZoulias : Yes, because even if 2 threads can enter
WaitAsync
at the same time, they cannot pass theQ
access at the same time. Notice thatQ
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. -
Theodor Zoulias over 2 yearsI am talking about this line:
if (IsSignaled) { IsSignaled = false; return; }
. This is not protected by a lock. TheIsSignaled
is not even avolatile
field. As for theif (Q.Contains(s))
, if you are sure that thes
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 thecancellationToken
is canceled and theWaitAsync
throws? -
Harry over 2 yearsYou've found some interesting edge cases. I'll try to fix them and edit my example... BRB.
-
Harry over 2 yearsFixed. I had to add
if (Q.Count > 0)
becausePeek()
throws when Q is empty, and it is empty in most common case theSet()
is invoked. -
Theodor Zoulias over 2 yearsYour 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 theWaitAsync
and another invokes theSet
. 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 theSet
is invoked by the second thread just after the first thread has executed theCheckSignaled()
, and just before executing theQ.Enqueue(s = new(0, 1))
. -
Theodor Zoulias over 2 yearsBtw 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. Are you an expert in coherency protocols, 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. -
Harry over 2 yearsThe
Reset
seems to be redundant here, so it goes. About the race condition - I give up.WaitAsync
can't blockSet()
. So how could I possibly prevent that particular condition from happening? BTW, that would happen only if I invokeSet()
parallelly withWaitAsync()
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 beforeWaitAsync()
entersEnqueue()
part. Even if it throws. -
Theodor Zoulias over 2 yearsHarry 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'sAsyncAutoResetEvent
? -
Harry over 2 yearsBTW, 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 inAutoResetEvent
class in the first place. -
Harry over 2 yearsWhy? 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. -
Theodor Zoulias over 2 yearsHmm, Stephen Cleary's
AsyncAutoResetEvent
supportsCancellationToken
s, so the timeout functionality can be built on top of that. I just posted here an extension method that supports timeout andCancellationToken
(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. -
Harry over 2 yearsThank 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.
-
Hugh Jeffner over 2 yearsAm I mistaken or is it not auto-resetting where it returns true after
if (winner == tcs.Task)
? -
Hugh Jeffner over 2 yearsNever mind, I didn't understand how it works. Turns out if there are tasks waiting it never bothers setting
isSignaled
-
vpalmu about 2 yearsNote 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.