【问题标题】:Awaiting a single .NET event with a ValueTask等待带有 ValueTask 的单个 .NET 事件
【发布时间】:2021-02-26 14:04:16
【问题描述】:

我有一个简单的ITimer 接口,它只有一个经典的Elapsed .NET 事件,该事件在特定时间跨度后引发。

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

}

现在我想创建一个类似于Task.Delay(TimeSpan) 的功能,但它应该使用ITimer 抽象而不是“实时”时间。也不同于Task.Delay(TimeSpan) 我虽然返回ValueTask 而不是Task 可能是个好主意,因为它可以实现无分配并且通常可能更高性能。无论如何,延迟的完成也只需要等待一次。

现在一个有点幼稚的实现可能看起来像这样。这里的问题是,使用ValueTask 根本没有任何好处。这样的实现怎么会充分利用ValueTask 所承诺的(便宜且无需分配)。

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);
}

【问题讨论】:

  • 我希望这个方法在你调用它时永远不会完成,所以这会比仅仅返回一个 Task 更糟糕。
  • 对不起,我不明白你的意思。
  • “过早的优化真的是万恶之源” (Donald Knuth)。我会非常小心地优化这样的事情,没有充分的理由相信这是一个实际问题。

标签: c# .net-core taskcompletionsource valuetask


【解决方案1】:

ManualResetValueTaskSourceCore&lt;TResult&gt; 上的内容不多。它本质上是一个TaskCompletionSource&lt;T&gt;,但对于ValueTask&lt;T&gt;

它确实打算在内部像你的计时器这样的类型使用,但如果你想将它用作外部方法,你可以create a wrapper for it

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);
}

然后您可以这样使用它(为简单起见,取消删除):

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);
}

请注意,由于这是一个外部方法,您仍将分配 ManualResetValueTaskSource&lt;T&gt;(以及委托)。所以这并不能真正给你买任何东西,我会选择在这里使用TaskCompletionSource&lt;T&gt;。当您添加 CancellationToken 支持并且必须处理计时器触发 (vts.SetResult) 和取消 (vts.SetException) 之间的竞争条件时尤其如此,因为我很确定只有其中一个应该发生每个 ValueTask 操作,我们的 ManualResetValueTaskSource&lt;T&gt; 中没有 Try* 变体,就像 TaskCompletionSource&lt;T&gt; 一样。

ValueTask 确实是为了成为一等公民;即,理想情况下,您将更新实际的计时器实现(和接口)以在其自己的类中而不是外部方法中支持ValueTask Delay(TimeSpan)。在计时器实例内部,它可以重用ValueTask 支持类型(ManualResetValueTaskSource&lt;T&gt;),它可以知道何时调用Reset,它可以完全避免委托/事件。

【讨论】:

  • 我试图避免在ITimer 抽象中添加一个额外的方法Delay(),因为我认为这个功能可以构建在当前非常简单的ITimer 抽象之上。但我想这会以性能为代价?
  • @bitbonk:是的,这就是权衡。不过,对于计时器,我认为Task 很好。我不希望它以足够短的延迟被频繁调用,以至于值得优化该分配。
  • 这让我想知道,为什么他们选择使用IAsyncDisposable.DisposeAsync 来返回ValueTask
  • 我认为其中的原因是有许多 IAsyncDisposable 类型根本没有任何实际处置。 IAsyncDisposable 多年来一直被避免使用,但最终为IAsyncEnumerable&lt;T&gt; 添加(需要异步处理)。大多数IAsyncDisposable 类型是异步流(至少现在是这样),并且大多数异步流不需要处理。所以那些IAsyncDisposable.DisposeAsync 方法将同步完成,ValueTask 在这种情况下避免分配。
猜你喜欢
  • 2019-02-24
  • 1970-01-01
  • 2017-03-13
  • 2021-07-02
  • 2011-12-28
  • 2013-07-12
  • 1970-01-01
  • 2011-08-03
  • 1970-01-01
相关资源
最近更新 更多