59

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?

2
  • 3
    @bside You're misrepresenting and the linked post. It correctly states that continuations in async functions are scheduled on the captured context. But in most cases that context is the default SynchronizationContext, which schedules continuations onto the thread pool, causing them to run in parallel. And even in WPF and ASP apps you can work around it with ConfigureAwait(false). Task.Run is used with CPU bound tasks and you don't need it to run continuations in parallel. Commented Mar 1, 2020 at 15:15
  • @V0ldek Yes, you are right. Thank you for drawing my attention to this. Commented Mar 24, 2020 at 17:28

4 Answers 4

61

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.

Sign up to request clarification or add additional context in comments.

8 Comments

await Task.WhenAll will return Thing[], so there's no need for Result (in fact, Result will wrap exceptions; you should use await or use the result of await Task.WhenAll).
Nice catch, thank you - it is early and I've been "awaiting" my coffee. :)
Whilst this is the generally correct way, what is this doing that return new List<Thing>() { await first, await second }; is not? If OP said that didn't work, there must be something else at play...
This doesn't work for me. These methods are still executing sequentially. I can only seem to achieve what I want using those tasks. Would the inner implementation of GetExpensiveThing(), an async method itself, have any effect on this?
@davenewza what does the implementation of GetExpensiveThing look like?
|
39

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

12 Comments

Nice! This is perfect is you want to use Stephen Toub's approach but prefer to use method-syntax LINQ.
I am glad you like it, that's exactly what I was going for :)
The AsParallel is redundant. The Select that follows is doing no CPU-intensive work. Also by removing the Task.Run from Stephen Toub's code you risk a reduced degree of parallelism in case the method funcBody is CPU-intensive and completes synchronously.
AsParallel takes care of Task.Run and Select combined @TheodorZoulias
@DanHunex replacing the Task.Run with AsParallel introduces artificial limits to the degree of parallelism, because the configuration WithDegreeOfParallelism is missing, so the default Environment.ProcessorCount is used. Take a look at this fiddle, where the requested maxDoP: 10 is not respected. Then comment the line .ParallelForEachAsync_HouseCat and uncomment the line //.ParallelForEachAsync_StephenToub, and see that now the requested DOP is respected.
|
3

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

Check this question here for reference

Comments

3

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.