有没有等价的Task.WhenAll
接受ValueTask
?
我可以使用
Task.WhenAll(tasks.Select(t => t.AsTask()))
如果它们都包装 a 这会很好,Task
但它会强制Task
为 real 分配一个无用的对象ValueTask
。
有没有等价的Task.WhenAll
接受ValueTask
?
我可以使用
Task.WhenAll(tasks.Select(t => t.AsTask()))
如果它们都包装 a 这会很好,Task
但它会强制Task
为 real 分配一个无用的对象ValueTask
。
按照设计,没有。从文档:
当方法的操作结果很可能同步可用并且方法被期望如此频繁地调用以致为每次调用分配新任务的成本将过高时,方法可能会返回此值类型的实例。
…</p>
例如,考虑一个方法,它可以返回
Task<TResult>
带有缓存任务的 a 作为公共结果,也可以返回ValueTask<TResult>
. 如果结果的使用者想将其用作 aTask<TResult>
,例如在Task.WhenAll
and之类的方法中使用 withTask.WhenAny
,则ValueTask<TResult>
首先需要将 the 转换为Task<TResult>
usingAsTask
,这会导致如果使用了缓存Task<TResult>
则可以避免的分配首先。
除非我遗漏了什么,否则我们应该能够在循环中等待所有任务:
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
分配 a。所以这里发生的唯一“额外”分配是我们用于返回结果的数组。
Order
返回项目的顺序与产生它们的给定任务的顺序相同。
异常
当一个任务抛出异常时,上面的代码将停止等待其余的异常并直接抛出。如果这是不可取的,我们可以这样做:
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) =>
WhenAll(tasks?.ToList() ?? throw new ArgumentNullException(nameof(tasks)));
// Arrays already implement IReadOnlyList<T>, but this overload
// is still useful because of params that allows callers to
// pass individual tasks like they are different arguments.
public static ValueTask<T[]> WhenAll<T>(
params ValueTask<T>[] tasks) =>
WhenAll(tasks as IReadOnlyList<ValueTask<T>>);
Theodor 在评论中提到了将结果数组/列表作为参数传递的方法,因此我们的实现将没有所有额外的分配,但调用者仍然必须创建它,如果他们批处理等待任务,这可能是有意义的,但这听起来就像一个相当专业的场景,所以如果你发现自己需要,你可能不需要这个答案
正如@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
不变,如果需要,可以轻松更改以实现它。
尝试做一些优化,结果返回正确的顺序和正确的异常处理。
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;
}
}
我正在使用这种扩展方法:
internal static class ValueTaskExtensions
{
public static Task WhenAll(this IEnumerable<ValueTask> tasks)
{
return Task.WhenAll(tasks.Select(v => v.AsTask()));
}
}