除非我遗漏了什么,否则我们应该能够在一个循环中等待所有任务:
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<ValueTask<T>> 和IReadOnlyList<ValueTask<T>> 以获得更广泛的兼容性。
样本签名:
// 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 提到了将结果数组/列表作为参数传递的方法,因此 我们的 实现将没有所有额外的分配,但调用者仍然必须创建它,这可能如果他们批量等待任务是有道理的,但这听起来像是一个相当专业的场景,所以如果你发现自己需要,你可能不需要这个答案?