7

On a pipeline in my app there is an option to inject a collection of pre-request handlers, that can mutate the request as it first enters the pipeline. The pipeline is fully async, and therefore these pre-request handler calls (as well as everything else) must be awaited. I have a few different ways to call these handlers, and I wondered if there is any difference between them, and if so what would the difference be? For example, would the various handlers be called in the same order? Which option might offer the best performance?

Option 1: foreach

foreach (var handler in this.preRequestHandlers)
{
    await handler.Handle(request);
}

Option 2: ForEach()

this.preRequestHandlers.ForEach(async handler => await handler.Handle(request));

Option 3: Task.WhenAll()

await Task.WhenAll(this.preRequestHandlers.Select(handler => handler.Handle(request)));
James
  • 1,028
  • 9
  • 20
  • [Task.WhenAll Method](https://msdn.microsoft.com/en-us/library/system.threading.tasks.task.whenall(v=vs.110).aspx) Will wait that all tasks end and they run in different threads, potentially concurrently, your foreach runs one by one in sequence.. – rmjoia Oct 20 '17 at 08:17
  • 1
    @rmjoia The code here isn't creating any additional threads. All of these solutions use exactly the same number of threads (namely the one calling thread, plus however many `handler.Handle` creates, which is probably none). – Servy Oct 20 '17 at 21:03

2 Answers2

8

Assuming you gain by running the individual handlers concurrently (you will know whether this is true), option 3 is best, as it will create a collection of Tasks in near-zero time, which will then potentially execute in parallel, then await once for all the tasks to finish. Option 1 will execute each handler in turn, serially. So if each handler takes say 4 seconds, and running many concurrently still takes ~4 seconds, option 3 will complete in ~4 seconds. Option 1 will complete in 4 x (number of pre-request handlers) seconds.

If you need the handlers to execute in order, one after the other, with no more than one handler executing at one time, then option 3 is not suitable.

Option 2 is not really suitable for anything, as it will complete before the handler tasks are complete. For the reason why, see any number of existing SO answers; e.g.:

How can I use Async with ForEach?

C# async await using LINQ ForEach()

Any point to List<T>.ForEach() with Async?

sellotape
  • 8,034
  • 2
  • 26
  • 30
1

The first 2 options will start and await all the tasks from the current thread, while the 3rd one generate a single task (that awaits tasks inside) that is awaited from the current thread.

The 2nd has call an additional function, that invokes another function (the lambda) foreach element, so it's an additional overhead from option 1.

In the 3rd the original collection is projected into another collection (with Select) that slows down compared to first 2 options.


So I think the best way is the Option 1 that reduces the tasks creations and functions stacking overhead.

As a performance improvement of 1st option I suggest you to use for loop instead of foreach (good explanation here)

Luca Corradi
  • 2,031
  • 1
  • 16
  • 17
  • 1
    All three of the shown methods behave radically differently, and not just in performance, but in what they actually do, and what their observable results are. This answer is very wrong and very dangerous as a result of that. – Servy Oct 20 '17 at 21:04