【问题标题】:Task.WhenAll for ValueTaskTask.WhenAll 用于 ValueTask
【发布时间】:2022-04-22 22:55:39
【问题描述】:

是否有相当于Task.WhenAll 接受ValueTask

我可以使用

Task.WhenAll(tasks.Select(t => t.AsTask()))

如果它们都包装了Task,这会很好,但它会强制为真正的ValueTask 分配一个无用的Task 对象。

【问题讨论】:

  • Task.WhenAll(tasks.Where(t => !t.IsCompletedSuccessfully).Select(t => t.AsTask()))
  • Şafak Gür 的answer 应该是接受的恕我直言!

标签: c# task-parallel-library valuetask


【解决方案1】:

按照设计,没有。来自the docs

当方法的操作结果很可能同步可用并且方法被期望如此频繁地调用以致为每次调用分配新任务的成本将过高时,方法可能会返回此值类型的实例.

例如,考虑一个方法,它可以返回带有缓存任务的Task<TResult> 作为常见结果,也可以返回ValueTask<TResult>。如果结果的消费者想要将其用作Task<TResult>,例如在Task.WhenAllTask.WhenAny 等方法中使用,则首先需要使用@987654330 将ValueTask<TResult> 转换为Task<TResult> @,如果一开始就使用了缓存的Task<TResult>,这将导致本可以避免的分配。

【讨论】:

  • 虽然很高兴知道它是设计使然,但我没有看到任何理由。除了我们自己实现之外,还不清楚什么是缓存任务的简单方法。如果Task.WhenAnyTask.WhenAllValueTasks 的重载,事情会容易得多。
  • 这个答案may have changed
  • @Nathan 在考虑了@marc-gravell 在该链接和其他内容中提到的警告之后,我已经开始在我的 API 中使用 ValueTask。但是仍然没有相当于Task.WhenAll...
  • @stuartd,同意 - 只是指出围绕该主题的文档正在......老化:)
【解决方案2】:

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

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

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

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

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

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

额外注意事项

  • 我们可以将此作为扩展方法。
  • 我们可以让重载接受IEnumerable&lt;ValueTask&lt;T&gt;&gt;IReadOnlyList&lt;ValueTask&lt;T&gt;&gt; 以获得更广泛的兼容性。

样本签名:

// There are some collections (e.g. hash-sets, queues/stacks,
// linked lists, etc) that only implement I*Collection interfaces
// and not I*List ones, but A) we're not likely to have our tasks
// in them and B) even if we do, IEnumerable accepting overload
// below should handle them. Allocation-wise; it's a ToList there
// vs GetEnumerator here.
public static async ValueTask<T[]> WhenAll<T>(
    IReadOnlyList<ValueTask<T>> tasks)
{
    // Our implementation above.
}

// ToList call below ensures that all tasks are initialized, so
// calling this with an iterator wouldn't cause the tasks to run
// sequentially (Thanks Sergey from comments to mention this
// possibility, which led me to add this Considerations section).
public static ValueTask<T[]> WhenAll<T>(
    IEnumerable<ValueTask<T>> tasks)
{
    return WhenAll(tasks?.ToList());
}

// Arrays already implement IReadOnlyList<T>, but this overload
// is still useful because as the `params` keyword allows callers 
// to pass individual tasks like they are different arguments.
public static ValueTask<T[]> WhenAll<T>(
    params ValueTask<T>[] tasks)
{
    return WhenAll(tasks as IReadOnlyList<ValueTask<T>>);
}

cmets 中的 Theodor 提到了将结果数组/列表作为参数传递的方法,因此 我们的 实现将没有所有额外的分配,但调用者仍然必须创建它,这可能如果他们批量等待任务是有道理的,但这听起来像是一个相当专业的场景,所以如果你发现自己需要,你可能不需要这个答案?

【讨论】:

  • 非常好!我删除了我的答案,因为你的答案更好。我认为您的 WhenAll 的最后一个版本,即抛出 AggregateException 的版本,具有大多数开发人员所期望的行为。
  • 您的代码会运行缓慢,因为您正在一一等待任务。例如,您有一百个任务,其中每个任务在百分之一秒内同步完成或在一秒内异步完成。如果 100 个任务中有 50 个碰巧是异步的,那么您的实现将花费 50.5 秒。 Task.WhenAll 将花费 1 秒。 Task.WhenAll != 循环等待。您不应等待上一个任务完成才能启动下一个任务。
  • @Sergey.quixoticaxis.Ivanov - Your code would work slowly, because you're awaiting the tasks one by one - 是的,但我不会一一开始; .NET 任务在您创建它们时开始运行,它们不会等待您等待它们。这意味着当调用此方法时,传递的任务已经在同时运行(即处于热状态)。因此我们只等待数组中最长的任务。
  • @Sergey.quixoticaxis.Ivanov - 这是一个快速示例:dotnetfiddle.net/DpWdC0
  • 啊,对不起,我的错。我正在考虑处理 IEnumerable&lt;ValueTask&gt; 这将是一个问题。
【解决方案3】:

正如@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;
}

当然,这只会有助于高吞吐量和大量ValueTask,因为它会增加一些其他开销。

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

【讨论】:

  • 结果的集合也可能与原始值任务的顺序不同。
  • @StephenCleary 考虑到了这一点,但我并不是说它是 Task.WhenAll 的精确替代品,不过它可以很容易地修复。
  • 讽刺的是,使用ValueTask 来减少分配,却不得不求助于分配来使用它。
  • 我什至可以说在这种情况下使用 Task 可能比使用像这样的辅助方法更有效,这些方法不仅分配一个类型的实例,它们分配它们的多个数组。我的猜测是,在许多/大多数情况下,这种方法的性能比仅使用 Task 更差,所以我建议任何人在选择使用什么之前衡量这两种方法的性能。
【解决方案4】:

我正在使用这种扩展方法:

internal static class ValueTaskExtensions
{
    public static Task WhenAll(this IEnumerable<ValueTask> tasks)
    {
        return Task.WhenAll(tasks.Select(v => v.AsTask()));
    }
}

【讨论】:

  • 这不能满足问题,因为调用 AsTask() 会导致分配,因此无法使用 ValueTask。
【解决方案5】:

尝试做一些优化,结果返回正确的顺序和正确的异常处理。

public static ValueTask<T[]> WhenAll<T>(IEnumerable<ValueTask<T>> tasks)
    {
        var list = tasks.ToList();
        var length = list.Count;
        var result = new T[length];
        var i = 0;

        for (; i < length; i ++)
        {
            if (list[i].IsCompletedSuccessfully)
            {
                result[i] = list[i].Result;
            }
            else
            {
                return WhenAllAsync();
            }
        }

        return new ValueTask<T[]>(result);

        async ValueTask<T[]> WhenAllAsync()
        {
            for (; i < length; i ++)
            {
                try
                {
                    result[i] = await list[i];
                }
                catch
                {
                    for (i ++; i < length; i ++)
                    {
                        try
                        {
                            await list[i];
                        }
                        catch
                        {
                            // ignored
                        }
                    }

                    throw;
                }
            }

            return result;
        }
    }

【讨论】:

  • 三重嵌套循环是完全没有必要的,因为所有循环都使用相同的循环控制变量,所以它永远不会起作用。
  • @BenVoigt,它不是三重嵌套循环。请学习 for(;;) 语句的基础知识。这是链接w3schools.com/cs/cs_for_loop.php
  • 循环内的函数调用内的循环内的catch内有一个循环。这是一个三重嵌套循环,即使共享控制变量意味着只通过序列进行一次传递。没有嵌套会更好更清晰,只需步骤1:保存领先的已完成任务的结果,步骤2:等待剩余任务保存结果,步骤3:如果出现异常,等待其他任务解决忽略结果(因为控制通过异常离开函数时没有返回值)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-26
  • 1970-01-01
  • 1970-01-01
  • 2020-04-07
  • 1970-01-01
相关资源
最近更新 更多