Running async methods in parallel

49,812

Solution 1

Is there a better to run async methods in parallel, or are tasks a good approach?

Yes, the "best" approach is to utilize the Task.WhenAll method. However, your second approach should have ran in parallel. I have created a .NET Fiddle, this should help shed some light. Your second approach should actually be running in parallel. My fiddle proves this!

Consider the following:

public Task<Thing[]> GetThingsAsync()
{
    var first = GetExpensiveThingAsync();
    var second = GetExpensiveThingAsync();

    return Task.WhenAll(first, second);
}

Note

It is preferred to use the "Async" suffix, instead of GetThings and GetExpensiveThing - we should have GetThingsAsync and GetExpensiveThingAsync respectively - source.

Solution 2

Task.WhenAll() has a tendency to become unperformant with large scale/amount of tasks firing simultaneously - without moderation/throttling.

If you are doing a lot of tasks in a list and wanting to await the final outcome, then I propose using a partition with a limit on the degree of parallelism.

I have modified Stephen Toub's blog elegant approach to modern LINQ:

public static Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, Task> funcBody, int maxDoP = 4)
{
    async Task AwaitPartition(IEnumerator<T> partition)
    {
        using (partition)
        {
            while (partition.MoveNext())
            {
                 await Task.Yield(); // prevents a sync/hot thread hangup
                 await funcBody(partition.Current);
            }
        }
    }

    return Task.WhenAll(
        Partitioner
            .Create(source)
            .GetPartitions(maxDoP)
            .AsParallel()
            .Select(p => AwaitPartition(p)));
}

How it works is simple, take an IEnumerable - dissect it into evenish partitions and the fire a function/method against each element, in each partition, at the same time. No more than one element in each partition at anyone time, but n Tasks in n partitions.

Extension Usage:

await myList.ParallelForEachAsync(myFunc, Environment.ProcessorCount);

Edit: I now keep some overloads in a repository on Github if you need more options. It's in a NuGet too for NetStandard.

Edit 2: Thanks to comments from Theodor below, I was able to mitigate poorly written Async Tasks from blocking parallelism by using await Task.Yield();.

Solution 3

You can your the Task.WhenAll, which returns when all depending tasks are done

Check this question here for reference

Solution 4

If GetExpensiveThing is properly asynchronous (meaning it doesn't do any IO or CPU work synchronously), your second solution of invoking both methods and then awaiting the results should've worked. You could've also used Task.WhenAll.

However, if it isn't, you may get better results by posting each task to the thread-pool and using the Task.WhenAll combinator, e.g.:

public Task<IList<Thing>> GetThings() =>
    Task.WhenAll(Task.Run(() => GetExpensiveThing()), Task.Run(() => GetExpensiveThing()));

(Note I changed the return type to IList to avoid awaits altogether.)

You should avoid using the Result property. It causes the caller thread to block and wait for the task to complete, unlike await or Task.WhenAll which use continuations.

Share:
49,812
Dave New
Author by

Dave New

Updated on July 09, 2022

Comments

  • Dave New
    Dave New almost 2 years

    I've got an async method, GetExpensiveThing(), which performs some expensive I/O work. This is how I am using it:

    // Serial execution
    public async Task<List<Thing>> GetThings()
    {
        var first = await GetExpensiveThing();
        var second = await GetExpensiveThing();
        return new List<Thing>() { first, second };
    }
    

    But since it's an expensive method, I want to execute these calls in in parallel. I would have thought moving the awaits would have solved this:

    // Serial execution
    public async Task<List<Thing>> GetThings()
    {
        var first = GetExpensiveThing();
        var second = GetExpensiveThing();
        return new List<Thing>() { await first, await second };
    }
    

    That didn't work, so I wrapped them in some tasks and this works:

    // Parallel execution
    public async Task<List<Thing>> GetThings()
    {
        var first = Task.Run(() =>
        {
            return GetExpensiveThing();
        });
    
        var second = Task.Run(() =>
        {
            return GetExpensiveThing();
        });
    
        return new List<Thing>() { first.Result, second.Result };
    }
    

    I even tried playing around with awaits and async in and around the tasks, but it got really confusing and I had no luck.

    Is there a better to run async methods in parallel, or are tasks a good approach?