Stefano d'Antonio
Stefano d'Antonio

Reputation: 6172

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.

Upvotes: 50

Views: 20324

Answers (5)

stuartd
stuartd

Reputation: 73303

By design, no. From the docs: (link updated)

A method may return an instance of this value type when it's likely that the result of its operation will be available synchronously, and when it's expected to be invoked so frequently that the cost of allocating a new Task<TResult> 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> in a method like WhenAll or WhenAny, the ValueTask<TResult> must first be converted to a Task<TResult> using AsTask(), leading to an allocation that would have been avoided if a cached Task<TResult> had been used in the first place.

Upvotes: 31

Şafak G&#252;r
Şafak G&#252;r

Reputation: 7365

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)
{
    ArgumentNullException.ThrowIfNull(tasks);
    if (tasks.Length == 0)
        return Array.Empty<T>();

    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.

Concurrency
Even though it looks like we execute the tasks sequentially, this is not really the case as the tasks are already started (i.e. are in a hot state) when this method is called. Therefore we only wait as long as the longest task in the array (thanks Sergey for asking about this in the comments).

Exceptions
When a task throws an exception, the above code would stop waiting for the rest of the tasks and just throw. If this is undesirable, we could do:

public static async ValueTask<T[]> WhenAll<T>(params ValueTask<T>[] tasks)
{
    ArgumentNullException.ThrowIfNull(tasks);
    if (tasks.Length == 0)
        return Array.Empty<T>();

    // 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(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.
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 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>>);
}

Theodor in the 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. This could make sense if they batch await tasks, which sounds like a fairly specialized scenario to me, but for completeness' sake:

// Arrays implement `IList<T>`
public static async ValueTask WhenAll<T>(ValueTask<T>[] source, IList<T> target)
{
    ArgumentNullException.ThrowIfNull(source);
    ArgumentNullException.ThrowIfNull(target);

    if (source.Length != target.Count)
        throw new ArgumentException(
            "Source and target lengths must match",
            nameof(target));

    List<Exception>? exceptions = null;

    for (var i = 0; i < source.Length; i++)
        try
        {
            target[i] = await source[i].ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            exceptions ??= new(source.Length);
            exceptions.Add(ex);
        }

    if (exceptions is not null)
        throw new AggregateException(exceptions);
}

Upvotes: 25

ded&#39;
ded&#39;

Reputation: 703

I'm using this extension method:

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

Upvotes: -4

xtadex
xtadex

Reputation: 116

Tried to do some optimization, result returned in correct order and correct exception handling.

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

Upvotes: -6

Stefano d&#39;Antonio
Stefano d&#39;Antonio

Reputation: 6172

As @stuartd pointed out, it is not supported by design, I had to implement this manually:

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

Of course this will help only in high throughput and high number of ValueTask as it adds some other overheads.

NOTE: As @StephenCleary pointed out, this does not keep the order as Task.WhenAll does, if it is required it can be easily changed to implement it.

Upvotes: 10

Related Questions