How to use C#8 IAsyncEnumerable to async-enumerate tasks run in parallel

后端 未结 5 1839
孤独总比滥情好
孤独总比滥情好 2021-02-09 15:00

If possible I want to create an async-enumerator for tasks launched in parallel. So first to complete is first element of the enumeration, second to finish is second element of

5条回答
  •  难免孤独
    2021-02-09 15:27

    My take on this task. Borrowed heavily from other answers in this topic, but with (hopefully) some enhancements. So the idea is to start tasks and put them in a queue, same as in the other answers, but like Theodor Zoulias, I'm also trying to limit the max degree of parallelism. However I tried to overcome the limitation he mentioned in his comment by using task continuation to queue the next task as soon as any of the previous tasks completes. This way we are maximizing the number of simultaneously running tasks, within the configured limit, of course.

    I'm not an async expert, this solution might have multithreading deadlocks and other Heisenbugs, I did not test exception handling etc, so you've been warned.

    public static async IAsyncEnumerable ExecuteParallelAsync(IEnumerable> coldTasks, int degreeOfParallelism)
    {
        if (degreeOfParallelism < 1)
            throw new ArgumentOutOfRangeException(nameof(degreeOfParallelism));
    
        if (coldTasks is ICollection>) throw new ArgumentException(
            "The enumerable should not be materialized.", nameof(coldTasks));
    
        var queue = new ConcurrentQueue>();
    
        using var enumerator = coldTasks.GetEnumerator();
        
        for (var index = 0; index < degreeOfParallelism && EnqueueNextTask(); index++) ;
    
        while (queue.TryDequeue(out var nextTask)) yield return await nextTask;
    
        bool EnqueueNextTask()
        {
            lock (enumerator)
            {
                if (!enumerator.MoveNext()) return false;
    
                var nextTask = enumerator.Current
                    .ContinueWith(t =>
                    {
                        EnqueueNextTask();
                        return t.Result;
                    });
                queue.Enqueue(nextTask);
                return true;
            }
        }
    }
    

    We use this method to generate testing tasks (borrowed from DK's answer):

    IEnumerable> GenerateTasks(int count)
    {
        return Enumerable.Range(1, count).Select(async n =>
        {
            Console.WriteLine($"#{n} started");
            await Task.Delay(new Random().Next(100, 1000));
            Console.WriteLine($"#{n} completed");
            return n;
        });
    }
    

    And also his(or her) test runner:

    async void Main()
    {
        await foreach (var n in ExecuteParallelAsync(GenerateTasks(9),3))
        {
            Console.WriteLine($"#{n} returned");
        }
    }
    

    And we get this result in LinqPad (which is awesome, BTW)

    #1 started
    #2 started
    #3 started
    #3 is complete
    #4 started
    #2 is complete
    #5 started
    #1 is complete
    #6 started
    #1 is returned
    #2 is returned
    #3 is returned
    #4 is complete
    #7 started
    #4 is returned
    #6 is complete
    #8 started
    #7 is complete
    #9 started
    #8 is complete
    #5 is complete
    #5 is returned
    #6 is returned
    #7 is returned
    #8 is returned
    #9 is complete
    #9 is returned
    

    Note how the next task starts as soon as any of the previous tasks completes, and how the order in which they return is still preserved.

提交回复
热议问题