如果我仍然使用 TaskCompletionSource 来实现异步,我应该返回 ValueTask 吗?

avo*_*avo 4 .net c# async-await .net-core

ValueTask如果我TaskCompletionSource用来实现实际的异步,返回还有好处吗?

据我了解, 的目的ValueTask是减少分配,但在 await 时分配仍然存在TaskCompletionSource.Task。这是一个简单的例子来说明这个问题。我应该从 返回Task(而不是ValueTaskDispatcherTimerDelayAsync,它本身总是被期望是异步的?

async ValueTask DispatcherTimerDelayAsync(TimeSpan delay) 
{
    var tcs = new TaskCompletionSource<bool>();
    var timer = new DispatcherTimer();
    timer.Interval = delay;
    timer.Tick += (s, e) => tcs.SetResult(true);
    timer.Start();
    try
    {
        await tcs.Task;
    }
    finally
    {
        timer.Stop();
    }
}
Run Code Online (Sandbox Code Playgroud)

Mar*_*ell 8

各有利弊。在“亲”栏中:

  1. 当同步返回结果(即Task<T>)时,使用ValueTask<T>避免了任务的分配 - 但是,这不适用于“void”(即非泛型Task),因为您可以只返回Task.CompletedTask
  2. 当您发出多个连续的可等待对象时,您可以使用带有连续令牌的单个“值任务源”来减少分配

(“2”的特殊情况可能是通过诸如 之类的工具进行摊销PooledAwait

它不觉得像这类原因适用于这里,但是:如有疑问,请记住,这是分配,自由地返回Task 作为一个ValueTaskreturn new ValueTask(task);没有更高版本),这将让你考虑改变实施的无偿划拨一破坏签名。当然,您仍然需要为原始Task等付费- 但将它们暴露ValueTask不会增加任何额外费用。

老实说,我不确定在这种情况下我是否会为此担心太多,因为延迟是一种分配方式。


Pet*_*ala 5

ValueTask是一个结构,而Task是一个类,这就是它可能减少分配的原因。( structs 大部分时间分配在堆栈上,因此当方法退出(退出 strackframe)时它们会自动消失。classes 大部分分配在堆上,必须执行GC CollectSweep来收集无法访问的对象。因此,如果您可以在堆栈上分配,那么您的堆内存分配就不会增长(因为该对象),这就是不触发 GC 的原因。

ValueTask当您的代码主要同步执行时,可以看到主要的好处。这意味着如果预期值已经存在,则无需创建承诺 ( TaskCompletionSource),该承诺将在将来实现。

您也无需担心分配问题,Task<bool>因为在这两种情况下,相应的任务对象都由运行时本身缓存。这同样适用于Task<int>-1 到 9 之间的数字。参考

简而言之,Task在这种情况下您应该使用而不是ValueTask.

  • @TheGeneral我不确定我会相信那个白痴,但我认为他总是[更喜欢 ValueTask 而不是 Task](https://blog.marcgravell.com/2019/08/prefer-valuetask-to-task -always-and.html) (3认同)
  • 这是一个很好的答案,当我开始阅读这篇文章时我非常怀疑,但你让我感到惊讶。我唯一要补充的是,除非您的基准测试和分析告诉您,您将受益于在热路径上使用 ValueTask 并且下游代码可以遵守其要求,那么您应该只使用常规的任务 (2认同)
  • @TheGeneral“以异步方式多次但顺序访问的东西”场景也是“ValueTask[&lt;T&gt;]”的一个非常引人注目的候选者 - 通过“源+令牌”方法 (2认同)
  • @MarcGravell 确实引人注目。生存的三件事:善待你的母亲、分配和线程池。并始终问自己“*MarcGravell 会做什么*”:P (2认同)
  • *“这同样适用于 `Task&lt;int&gt;`,但仅适用于 0 到 10 之间的数字。”* &lt;== 这是有文档记录的,还是您通过研究源代码知道的? (2认同)