0

I have a similair question to Running async methods in parallel in that I wish to run a number of functions from a list of functions in parallel.

I have noted in a number of comments online it is mentioned that if you have another await in your methods, Task.WhenAll() will not help as Async methods are not parallel.

I then went ahead and created a thread for each using function call with the below (the number of parallel functions will be small typically 1 to 5):

public interface IChannel
{
    Task SendAsync(IMessage message);
}

public class SendingChannelCollection
{
    protected List<IChannel> _channels = new List<IChannel>();

    /* snip methods to add channels to list etc */

    public async Task SendAsync(IMessage message)
    {
        var tasks = SendAll(message);

        await Task.WhenAll(tasks.AsParallel().Select(async task => await task));
    }

    private IEnumerable<Task> SendAll(IMessage message)
    {
        foreach (var channel in _channels)
            yield return channel.SendAsync(message, qos);
    }
}

I would like to double check I am not doing anything horrendous with code smells or bugs as i get to grips with what I have patched together from what i have found online. Many thanks in advance.

morleyc
  • 2,169
  • 10
  • 48
  • 108
  • 2
    *if you have another await in your methods, Task.WhenAll() will not help as Async methods are not parallel.* This statement is wrong. Async methods can be parallel if they are CPU-bound, and definitely can be concurrent if they are either CPU-bound or I/O-bound. It depends on how you start and then `await` the associated tasks. If you `await` each task immediately after its creation: no concurrency. If you create them all and then await them: concurrency. – Theodor Zoulias Mar 01 '20 at 12:23

1 Answers1

2

Let's compare the behaviour of your line:

await Task.WhenAll(tasks.AsParallel().Select(async task => await task));

in contrast with:

await Task.WhenAll(tasks);

What are you delegating to PLINQ in the first case? Only the await operation, which does basically nothing - it invokes the async/await machinery to wait for one task. So you're setting up a PLINQ query that does all the heavy work of partitioning and merging the results of an operation that amounts to "do nothing until this task completes". I doubt that is what you want.

If you have another await in your methods, Task.WhenAll() will not help as Async methods are not parallel.

I couldn't find that in any of the answers to the linked questions, except for one comment under the question itself. I'd say that it's probably a misconception, stemming from the fact that async/await doesn't magically turn your code into concurrent code. But, assuming you're in an environment without a custom SynchronizationContext (so not an ASP or WPF app), continuations to async functions will be scheduled on the thread pool and possibly run in parallel. I'll delegate you to this answer to shed some light on that. That basically means that if your SendAsync looks something like this:

Task SendAsync(IMessage message)
{
    // Synchronous initialization code.

    await something;

    // Continuation code.
}

Then:

  • The first part before await runs synchronously. If this part is heavyweight, you should introduce parallelism in SendAll so that the initialization code is run in parallel.
  • await works as usual, waiting for work to complete without using up any threads.
  • The continuation code will be scheduled on the thread pool, so if a few awaits finish up at the same time their continuations might be run in parallel if there's enough threads in the thread pool.

All of the above is assuming that await something actually awaits asynchronously. If there's a chance that await something completes synchronously, then the continuation code will also run synchronously.

Now there is a catch. In the question you linked one of the answers states:

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

Now I don't know if that's true, since I weren't able to find any other source claiming that. I guess it's possible and in that case it might actually be beneficial to invoke PLINQ to deal with partitioning and throttling for you. However, you said you typically handle 1-5 functions, so you shouldn't worry about this.

So to summarize, parallelism is hard and the correct approach depends on how exactly your SendAsync method looks like. If it has heavyweight initialization code and that's what you want to parallelise, you should run all the calls to SendAsync in parallel. Otherwise, async/await will be implicitly using the thread pool anyway, so your call to PLINQ is redundant.

V0ldek
  • 9,623
  • 1
  • 26
  • 57