Is there an equivalent of Task.WhenAll
accepting ValueTask
?
I can work around it using
Task.WhenAll(tasks.Select(t => t.AsTask()))
This will be fine if they're all wrapping a Task
but it will force the useless allocation of a Task
object for real ValueTask
.
As you know Task is a reference type and it is allocated on the heap but on the contrary, ValueTask is a value type and it is initialized on the stack so it would make a better performance in this scenario.
WhenAll returns control after all tasks are completed, while WhenAny returns control as soon as a single task is completed.
Task. WhenAll creates a task that will complete when all of the supplied tasks have been completed. It's pretty straightforward what this method does, it simply receives a list of Tasks and returns a Task when all of the received Tasks completes.
By design, no. From the docs:
Methods may return an instance of this value type when it's likely that the result of their operations will be available synchronously and when the method is expected to be invoked so frequently that the cost of allocating a new Task for each call will be prohibitive.
…
For example, consider a method that could return either a
Task<TResult>
with a cached task as a common result or aValueTask<TResult>
. If the consumer of the result wants to use it as aTask<TResult>
, such as to use with in methods likeTask.WhenAll
andTask.WhenAny
, theValueTask<TResult>
would first need to be converted into aTask<TResult>
usingAsTask
, which leads to an allocation that would have been avoided if a cachedTask<TResult>
had been used in the first place.
Unless there is something I'm missing, we should be able to just await all the tasks in a loop:
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; }
Allocations
Awaiting a ValueTask
that is completed synchronously shouldn't cause a Task
to be allocated. So the only "extra" allocation happening here is of the array we use for returning the results.
Order
Order of the returned items are the same as the order of the given tasks that produce them.
Exceptions
When a task throws an exception, the above code would stop waiting for the rest of the exceptions and just throw. If this is undesirable, we could do:
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); }
Extra considerations
IEnumerable<ValueTask<T>>
and IReadOnlyList<ValueTask<T>>
for wider compatibility.Sample signatures:
// 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 in comments mentioned the approach of having the result array/list passed as an argument, so our implementation would be free of all extra allocations but the caller will still have to create it, which could make sense if they batch await tasks but that sounds like a fairly specialized scenario, so if you find yourself needing that you probably don't need this answer 🙂
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With