Task.WhenAll for ValueTask

Ste*_*nio 14 c# task-parallel-library

是否有相同的Task.WhenAll接受ValueTask

我可以使用它来解决它

Task.WhenAll(tasks.Select(t => t.AsTask()))
Run Code Online (Sandbox Code Playgroud)

如果它们都包装了Task它会很好,但它会迫使Task对象无用地分配ValueTask.

stu*_*rtd 11

按设计,没有.来自文档:

方法可能会返回此值类型的实例,因为它们的操作结果可能同步可用,并且预计会频繁调用该方法,以致为每个调用分配新任务的成本将过高.

...

例如,考虑一种方法,该方法可以返回Task<TResult>带有缓存任务的a作为常见结果或者ValueTask<TResult>.如果结果的消费者想用它作为Task<TResult>,比如在类似的方法使用Task.WhenAllTask.WhenAny,则ValueTask<TResult>首先需要转换成Task<TResult>使用AsTask,这导致如果高速缓存会被避免了分配Task<TResult>已经使用首先.

因此,任何异步方法的默认选择应该是返回一个TaskTask<TResult>.只有在性能分析证明它值得的时候才应该ValueTask<TResult>使用而不是Task<TResult>.

  • 虽然很高兴知道这是设计使然,但我没有看到任何给出的推理。除了我们自己实现之外,还不清楚什么是缓存任务的简单方法。如果“Task.WhenAny”和“Task.WhenAll”具有“ValueTask”的重载,事情就会容易得多。 (5认同)
  • 这个答案[可能已更改](https://blog.marcgravell.com/2019/08/prefer-valuetask-to-task-always-and.html) (3认同)
  • @stuartd,同意 - 只是指出围绕该主题的文档是......老化:) (2认同)

Ste*_*nio 9

正如@stuartd 指出的那样,它不受设计支持,我不得不手动实现:

public static async Task<IReadOnlyCollection<T>> WhenAll<T>(this IEnumerable<ValueTask<T>> tasks)
{
    var results = new List<T>();
    var toAwait = new List<Task<T>>();

    foreach (var valueTask in tasks)
    {
        if (valueTask.IsCompletedSuccessfully)
            results.Add(valueTask.Result);
        else
            toAwait.Add(valueTask.AsTask());
    }

    results.AddRange(await Task.WhenAll(toAwait).ConfigureAwait(false));

    return results;
}
Run Code Online (Sandbox Code Playgroud)

当然,这只会在高吞吐量和高数量的情况下有所帮助,ValueTask因为它会增加一些其他开销。

注意:正如@StephenCleary 指出的那样,这不会保持顺序Task.WhenAll,如果需要,可以轻松更改以实现它。

  • 结果的集合也可能与原始值任务的顺序不同。 (3认同)
  • 具有讽刺意味的是,使用 `ValueTask` 来减少分配,但无论如何不得不求助于分配来使用它。 (2认同)
  • 我什至可以说,在这种情况下使用“Task”可能比使用此类辅助方法更有效,这些方法不仅分配类型的实例,还分配类型的多个数组。我的猜测是,在许多/大多数情况下,这种方法的性能比仅使用“Task”要差,因此我建议任何人在选择使用哪种方法之前先衡量任一方法的性能。 (2认同)

Şaf*_*Gür 7

除非我遗漏了什么,否则我们应该能够在循环中等待所有任务:

public static async ValueTask<T[]> WhenAll<T>(params ValueTask<T>[] tasks)
{
    // Argument validations omitted

    var results = new T[tasks.Length];
    for (var i = 0; i < tasks.Length; i++)
        results[i] = await tasks[i].ConfigureAwait(false);

    return results;
}
Run Code Online (Sandbox Code Playgroud)


等待 aValueTask同步完成的分配不应导致 aTask被分配。所以这里发生的唯一“额外”分配是我们用于返回结果的数组。

顺序
返回项目的顺序与产生它们的给定任务的顺序相同。

异常
当一个任务抛出异常时,上面的代码将停止等待其余的异常并直接抛出。如果这是不可取的,我们可以这样做:

public static async ValueTask<T[]> WhenAll<T>(params ValueTask<T>[] tasks)
{
    Exception? exception = null;

    var results = new T[tasks.Length];
    for (var i = 0; i < tasks.Length; i++)
        try
        {
            results[i] = await tasks[i].ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            // Remember the first exception, swallow the rest
            exception ??= ex;
        }

    return exception is null
        ? results
        : throw exception;
}
Run Code Online (Sandbox Code Playgroud)

我们直接抛出第一个异常,因为用 an 包裹它AggregateException不是ValueTask一回事。

Task<T>.Result备注

...如果在任务运行过程中发生异常,或者任务已被取消,则 Result 属性不会返回值。相反,尝试访问属性值会引发 AggregateException 异常

ValueTask<T>.Result备注

如果此 ValueTask 出现故障,则此属性会引发异常。抛出的异常未包含在 AggregateException 中

但是如果我们确实希望我们的WhenAll方法抛出一个AggregateException包含所有抛出的异常,我们可以这样做:

public static async ValueTask<T[]> WhenAll<T>(params ValueTask<T>[] tasks)
{
    // We don't allocate the list if no task throws
    List<Exception>? exceptions = null;

    var results = new T[tasks.Length];
    for (var i = 0; i < tasks.Length; i++)
        try
        {
            results[i] = await tasks[i].ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            exceptions ??= new List<Exception>(tasks.Length);
            exceptions.Add(ex);
        }

    return exceptions is null
        ? results
        : throw new AggregateException(exceptions);
}
Run Code Online (Sandbox Code Playgroud)

  • @Sergey.quixoticaxis.Ivanov - “你的代码会运行得很慢,因为你正在一一等待任务” - 是的,但我不会一一启动它们;.NET 任务在您创建它们时开始运行,它们不会等待您等待它们。这意味着当调用此方法时,传递的任务已经同时运行(即处于热状态)。因此我们只等待数组中最长的任务。 (3认同)
  • 很不错!我删除了我的答案,因为你的答案更优秀。我认为您的最后一个版本的“WhenAll”(抛出“AggregateException”)具有大多数开发人员所期望的行为。 (2认同)