I am trying to update my toolset with the new tools offered by C# 8, and one method that seems particularly useful is a version of Task.WhenAll that returns an IAsyncEnumera
By using code from this article, you can implement the following:
public static Task<Task<T>>[] Interleaved<T>(IEnumerable<Task<T>> tasks)
{
var inputTasks = tasks.ToList();
var buckets = new TaskCompletionSource<Task<T>>[inputTasks.Count];
var results = new Task<Task<T>>[buckets.Length];
for (int i = 0; i < buckets.Length; i++)
{
buckets[i] = new TaskCompletionSource<Task<T>>();
results[i] = buckets[i].Task;
}
int nextTaskIndex = -1;
Action<Task<T>> continuation = completed =>
{
var bucket = buckets[Interlocked.Increment(ref nextTaskIndex)];
bucket.TrySetResult(completed);
};
foreach (var inputTask in inputTasks)
inputTask.ContinueWith(continuation, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
return results;
}
Then change your WhenEach
to call the Interleaved
code
public static async IAsyncEnumerable<TResult> WhenEach<TResult>(Task<TResult>[] tasks)
{
foreach (var bucket in Interleaved(tasks))
{
var t = await bucket;
yield return await t;
}
}
Then you can call your WhenEach
as per usual
await foreach (int result in WhenEach(tasks))
{
Console.WriteLine($"Processed: {result}");
}
I did some rudimentary benchmarking with 10k tasks and performed 5 times better in terms of speed.
I really liked the solution provided by Panagiotis, but still wanted to get exceptions raised as they happen like in JohanP's solution.
To achieve that we can slightly modify that to try closing the channel in the continuations when a task fails:
public IAsyncEnumerable<T> ToAsyncEnumerable<T>(IEnumerable<Task<T>> inputTasks)
{
if (inputTasks == null)
{
throw new ArgumentNullException(nameof(inputTasks), "Task list must not be null.");
}
var channel = Channel.CreateUnbounded<T>();
var channelWriter = channel.Writer;
var inputTaskContinuations = inputTasks.Select(inputTask => inputTask.ContinueWith(completedInputTask =>
{
// Check whether the task succeeded or not
if (completedInputTask.Status == TaskStatus.RanToCompletion)
{
// Write the result to the channel on successful completion
channelWriter.TryWrite(completedInputTask.Result);
}
else
{
// Complete the channel on failure to immediately communicate the failure to the caller and prevent additional results from being returned
var taskException = completedInputTask.Exception?.InnerException ?? completedInputTask?.Exception;
channelWriter.TryComplete(taskException);
}
}));
// Ensure the writer is closed after the tasks are all complete, and propagate any exceptions from the continuations
_ = Task.WhenAll(inputTaskContinuations).ContinueWith(completedInputTaskContinuationsTask => channelWriter.TryComplete(completedInputTaskContinuationsTask.Exception));
// Return the async enumerator of the channel so results are yielded to the caller as they're available
return channel.Reader.ReadAllAsync();
}
The obvious downside to this is that the first error encountered will end enumeration and prevent any other, possibly successful, results from being returned. This is a tradeoff that's acceptable for my use case, but may not be for others.
Just for the fun of it, using System.Reactive and System.Interactive.Async:
public static async IAsyncEnumerable<TResult> WhenEach<TResult>(
Task<TResult>[] tasks)
=> Observable.Merge(tasks.Select(t => t.ToObservable())).ToAsyncEnumerable()
You can use a Channel as an async queue. Each task can write to the channel when it completes. Items in the channel will be returned as an IAsyncEnumerable through ChannelReader.ReadAllAsync.
IAsyncEnumerable<T> ToAsyncEnumerable<T>(IEnumerable<Task<T>> inputTasks)
{
var channel=Channel.CreateUnbounded<T>();
var writer=channel.Writer;
var continuations=inputTasks.Select(t=>t.ContinueWith(x=>
writer.TryWrite(x.Result)));
_ = Task.WhenAll(continuations)
.ContinueWith(t=>writer.Complete(t.Exception));
return channel.Reader.ReadAllAsync();
}
When all tasks complete writer.Complete()
is called to close the channel.
To test this, this code produces tasks with decreasing delays. This should return the indexes in reverse order :
var tasks=Enumerable.Range(1,4)
.Select(async i=>
{
await Task.Delay(300*(5-i));
return i;
});
await foreach(var i in Interleave(tasks))
{
Console.WriteLine(i);
}
Produces :
4
3
2
1