How to implement an efficient WhenEach that streams an IAsyncEnumerable of task results?

后端 未结 4 924
闹比i
闹比i 2021-01-01 04:32

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

相关标签:
4条回答
  • 2021-01-01 04:57

    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.

    0 讨论(0)
  • 2021-01-01 05:02

    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.

    0 讨论(0)
  • 2021-01-01 05:16

    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()
    
    0 讨论(0)
  • 2021-01-01 05:18

    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
    
    0 讨论(0)
提交回复
热议问题