Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Task.WhenAll for ValueTask

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.

like image 689
Stefano d'Antonio Avatar asked Aug 15 '17 08:08

Stefano d'Antonio


People also ask

What is the difference between task and 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.

Can you tell difference between task WhenAll and task WhenAny?

WhenAll returns control after all tasks are completed, while WhenAny returns control as soon as a single task is completed.

Does WhenAll start the tasks?

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.


2 Answers

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 a ValueTask<TResult>. If the consumer of the result wants to use it as a Task<TResult>, such as to use with in methods like Task.WhenAll and Task.WhenAny, the ValueTask<TResult> would first need to be converted into a Task<TResult> using AsTask, which leads to an allocation that would have been avoided if a cached Task<TResult> had been used in the first place.

like image 174
stuartd Avatar answered Sep 28 '22 03:09

stuartd


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

  • We can have this as an extension method.
  • We can have overloads that accept 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 🙂

like image 44
Şafak Gür Avatar answered Sep 28 '22 02:09

Şafak Gür