简体   繁体   中英

Awaiting a single .NET event with a ValueTask

I have a simple ITimer interface that has just a classic Elapsed .NET event that is raised after the a certain timespan.

interface ITimer
{
    void Start(TimeSpan interval);
    void Stop();
    public event EventHandler Elapsed;

}

Now I would like to create a functionality that is similar to Task.Delay(TimeSpan) but it should use that ITimer abstraction instead of the "real" time. Also different form Task.Delay(TimeSpan) I though it might be a good idea to return a ValueTask instead of a Task because it could be implemented allocation-free and might generally be more performant. Also the completion of the delay only needs to be awaited once anyway.

Now a somewhat naive implementation could probably look like this. There problem here is, that there is no benefit of using a ValueTask at all. How could an implementation look like that takes full benefit of what a ValueTask promises (cheap and allocation-free).

ValueTask Delay(TimeSpan duration, CancellationToken cancellationToken)
{
    // We get the ITimer instance from somwhere, doesn't matter for this question.
    ITimer timer = timerFactory.Create();
    var tcs = new TaskCompletionSource<bool>();
    timer.Elapsed += (_, __) => { tcs.SetResult(true); };
    cancellationToken.Register(() =>
    {
        timer.Stop();
        throw new OperationCanceledException();
    });
    timer.Start(duration);
    return new ValueTask(tcs.Task);
}

Not much has been written on ManualResetValueTaskSourceCore<TResult> . It's essentially a TaskCompletionSource<T> but for ValueTask<T> .

It's really intended to be used inside a type like your timer, but you can create a wrapper for it if you want to use it as an external method:

public sealed class ManualResetValueTaskSource<T> : IValueTaskSource<T>, IValueTaskSource
{
  private ManualResetValueTaskSourceCore<T> _logic; // mutable struct; do not make this readonly

  public bool RunContinuationsAsynchronously
  {
    get => _logic.RunContinuationsAsynchronously;
    set => _logic.RunContinuationsAsynchronously = value;
  }
  public void Reset() => _logic.Reset();
  public void SetResult(T result) => _logic.SetResult(result);
  public void SetException(Exception error) => _logic.SetException(error);

  short IValueTaskSource.Version => _logic.Version;
  short IValueTaskSource<T>.Version => _logic.Version;
  void IValueTaskSource.GetResult(short token) => _logic.GetResult(token);
  T IValueTaskSource<T>.GetResult(short token) => _logic.GetResult(token);
  ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _logic.GetStatus(token);
  ValueTaskSourceStatus IValueTaskSource<T>.GetStatus(short token) => _logic.GetStatus(token);

  void IValueTaskSource.OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => _logic.OnCompleted(continuation, state, token, flags);
  void IValueTaskSource<T>.OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => _logic.OnCompleted(continuation, state, token, flags);
}

You can then use it as such (cancellation removed for simplicity):

ValueTask Delay(TimeSpan duration)
{
  ITimer timer = timerFactory.Create();
  var vts = new ManualResetValueTaskSource<object>();
  TimerElapsed eventHandler = null;
  eventHandler = (_, __) =>
  {
    timer.Elapsed -= eventHandler;
    vts.SetResult(null);
  };
  timer.Elapsed += eventHandler;
  timer.Start(duration);
  return new ValueTask(vts);
}

Note that, since this is an external method, you'd still be allocating the ManualResetValueTaskSource<T> (as well as the delegate). So this doesn't really buy you anything, and I would choose to use TaskCompletionSource<T> here. This is especially true when you add CancellationToken support and have to handle the race condition between the timer firing ( vts.SetResult ) and the cancellation ( vts.SetException ), since I'm pretty sure only one of those is supposed to happen per ValueTask operation, and there's no Try* variants in our ManualResetValueTaskSource<T> like TaskCompletionSource<T> has.

ValueTask is really intended to be more of a first-class citizen; ie, ideally you would update the actual timer implementation (and interface) to support ValueTask Delay(TimeSpan) in its own class instead of an external method. Inside the timer instance, it can reuse the ValueTask backing type ( ManualResetValueTaskSource<T> ), it can know when to call Reset , and it can avoid the delegate/event completely.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM