0

I'm implementing an AsyncManualResetEvent based on Stephen Toub's example. However, I would like to know if the event, or specifically, the underlying Task<T> has been waited on.

I've already investigated the Task class, and there doesn't seem to be a sensible way to determine if it has ever been 'awaited' or if a continuation has been added.

In this case however, I control access to the underlying task source, so I can listen for any calls to the WaitAsync method instead. In thinking about how to do this, I decided to use a Lazy<T> and just see if it has been created.

sealed class AsyncManualResetEvent {
    public bool HasWaiters => tcs.IsValueCreated;

    public AsyncManualResetEvent() {
        Reset();
    }

    public Task WaitAsync() => tcs.Value.Task;

    public void Set() {
        if (tcs.IsValueCreated) {
            tcs.Value.TrySetResult(result: true);
        }
    }

    public void Reset() {
        tcs = new Lazy<TaskCompletionSource<bool>>(LazyThreadSafetyMode.PublicationOnly);
    }

    Lazy<TaskCompletionSource<bool>> tcs;
}

My question then, is whether this is a safe approach, specifically will this guarantee that there are never any orphaned/lost continuations while the event is being reset?

Robert Byrne
  • 562
  • 2
  • 13
  • `tcs` can change inside `Set()`. Lock-free code is not easy. – SLaks Feb 05 '16 at 20:01
  • Well as soon as I posted this, I realised that I've forgotten the check for `IsComplete` in the `Reset` method of the original implementation. In this case, calling `Reset` would just leave any outstanding waiters hanging. – Robert Byrne Feb 05 '16 at 20:01
  • @SLaks true, I had a feeling this is a little too simplistic – Robert Byrne Feb 05 '16 at 20:02
  • `I would like to know if the event, or specifically, the underlying Task has been waited on.` This is almost certainly the wrong solution. But you haven't described the actual *problem* you're trying so solve, so we can't help you find a better solution. – Stephen Cleary Feb 06 '16 at 00:41
  • Yes it was the wrong approach, the actual problem is kind of complicated to explain, but in short, there's a specific window in which work can be done, if you turn up before that window opens, you have to wait, if you turn up after it opens, then you're out of luck. So I'll set up another tcs after the ship has sailed, and then fail that task, so anyone waiting will be notified. – Robert Byrne Feb 06 '16 at 15:34

1 Answers1

4

If you truly wanted to know if anyone called await on your task (not just the fact that they called WaitAsync()) you could make a custom awaiter that acts as a wrapper for the TaskAwaiter that is used by m_tcs.Task.

public class AsyncManualResetEvent
{
    private volatile Completion _completion = new Completion();

    public bool HasWaiters => _completion.HasWaiters;

    public Completion WaitAsync()
    {
        return _completion;
    }

    public void Set()
    {
        _completion.Set();
    }

    public void Reset()
    {
        while (true)
        {
            var completion = _completion;
            if (!completion.IsCompleted ||
                Interlocked.CompareExchange(ref _completion, new Completion(), completion) == completion)
                return;
        }
    }
}

public class Completion
{
    private readonly TaskCompletionSource<bool> _tcs;
    private readonly CompletionAwaiter _awaiter;

    public Completion()
    {
        _tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
        _awaiter = new CompletionAwaiter(_tcs.Task, this);
    }

    public CompletionAwaiter GetAwaiter() => _awaiter;
    public bool IsCompleted => _tcs.Task.IsCompleted;
    public bool HasWaiters { get; private set; }
    public void Set() => _tcs.TrySetResult(true);

    public struct CompletionAwaiter : ICriticalNotifyCompletion
    {
        private readonly TaskAwaiter _taskAwaiter;
        private readonly Completion _parent;

        internal CompletionAwaiter(Task task, Completion parent)
        {
            _parent = parent;
            _taskAwaiter = task.GetAwaiter();
        }

        public bool IsCompleted => _taskAwaiter.IsCompleted;
        public void GetResult() => _taskAwaiter.GetResult();
        public void OnCompleted(Action continuation)
        {
            _parent.HasWaiters = true;
            _taskAwaiter.OnCompleted(continuation);
        }

        public void UnsafeOnCompleted(Action continuation)
        {
            _parent.HasWaiters = true;
            _taskAwaiter.UnsafeOnCompleted(continuation);
        }
    }
}

Now if anyone registered a continuation with OnCompleted or UnsafeOnCompleted the bool HasWaiters will become true.

I also added TaskCreationOptions.RunContinuationsAsynchronously to fix the issue Stephen fixes with the Task.Factory.StartNew at the end of the article (It was introduced to .NET after the article was written).


If you just want to see if anyone called WaitAsync you can simplify it a lot, you just need a class to hold your flag and your completion source.

public class AsyncManualResetEvent
{
    private volatile CompletionWrapper _completionWrapper = new CompletionWrapper();

    public Task WaitAsync()
    {
        var wrapper = _completionWrapper;
        wrapper.WaitAsyncCalled = true;
        return wrapper.Tcs.Task;
    }

    public bool WaitAsyncCalled
    {
        get { return _completionWrapper.WaitAsyncCalled; }
    }

    public void Set() {
        _completionWrapper.Tcs.TrySetResult(true); }

    public void Reset()
    {
        while (true)
        {
            var wrapper = _completionWrapper;
            if (!wrapper.Tcs.Task.IsCompleted ||
                Interlocked.CompareExchange(ref _completionWrapper, new CompletionWrapper(), wrapper) == wrapper)
                return;
        }
    }
    private class CompletionWrapper
    {
        public TaskCompletionSource<bool> Tcs { get; } = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
        public bool WaitAsyncCalled { get; set; }
    }
}
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • 1
    This is an XY problem. He really just needs to make sure that he can reset the event without orphaning a task. You do that by using a `lock`, not by trying to keep track of whether there are any continuations. And even having that information doesn't change the fact that the way to prevent orphaning a task is to lock around the code in his class (and also to unconditionally complete each created TCS). – Servy Feb 05 '16 at 21:51
  • The second example actually makes sense, the atomicity of the two things was the real trick, didn't occur to me to put them into a nested container and just reinstanciate that – Robert Byrne Feb 05 '16 at 21:54
  • @Servy yes and no, in fairness, I should just be locking (I'm probably going to use a different approach which might let me avoid the need to check for waiters). The real problem is that I want to 'catch out' people who await after a particular phase and complain, but I don't want their waits fulfilled. – Robert Byrne Feb 05 '16 at 21:56
  • @RobertByrne And that part comes from the second half of my answer, that of making sure to mark every TCS as completed before creating a new one, so you never have tasks that can't ever be completed. – Servy Feb 05 '16 at 23:25
  • @servy agreed, the whole approach I'm taking goes against the grain, I'll come at it another way. Thanks all the same Scott, I'll accept your answer. – Robert Byrne Feb 05 '16 at 23:33