0

I am looking to optimize a function which makes a bunch of HTTP IO calls to other APIs and does something with the response. These API calls are not dependent on each other, so is there a way to execute them in parallel and get some performance gains instead of awaiting each of these calls one by one immediately?

This function is part of an ASP.NET Core Web API and is invoked for certain incoming API requests.

Option 1 - Make all calls individually and await them immediately

var res1 = await AsyncIOCall1();
var res2 = await AsyncIOCall2();
var res3 = await AsyncIOCall2();
<Do something with response>

Option 2 - Make all calls but await them later

var resTask1 = AsyncIOCall1();
var resTask2 = AsyncIOCall2();
var resTask3 = AsyncIOCall2();

//Now await
var res1 = await resTask1;
var res2 = await resTask2;
var res3 = await restTask3;

<Do something with response>

Option 3 - Use Task.Run

var resTask1 = Task.Run(async () => await AsyncIOCall1());
var resTask2 = Task.Run(async () => await AsyncIOCall2());
var resTask3 = Task.Run(async () => await AsyncIOCall3());

//Now await
var res1 = await resTask1;
var res2 = await resTask2;
var res3 = await restTask3;

<Do something with response>

What is really going to be the difference between these options and is there a preferred option?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459

1 Answers1

2

The difference between these options revolves around the concurrency and asynchrony of the operations.

Option 1 - Make all calls individually and await them immediately:

In this scenario, you're sending requests sequentially. You're sending the first request, waiting for the response, sending the second request, waiting for its response, and so on. This method isn't taking advantage of parallelism, so it's going to be the slowest method if the calls are not dependent on each other.

Option 2 - Make all calls but await them later:

In this scenario, you're initiating all of the requests "at the same time", and then waiting for the responses. This will take roughly as long as the longest single operation, which can be significantly faster than Option 1 if the operations are long-running.

Option 3 - Use Task.Run:

This approach isn't recommended for I/O-bound operations. The Task.Run method is generally best for CPU-bound operations where you want to offload some work to a background thread. But in this case, you're just creating unnecessary threads. The operations are already asynchronous and non-blocking; wrapping them in Task.Run doesn't provide any benefits, and can actually decrease performance by adding overhead.

For your scenario I would improve the options 2 by using Task.WhenAll(). After all tasks completed, you can pull the results out individually with await (see more details on this approach Awaiting multiple Tasks with different results)

await Task.WhenAll(resTask1, resTask2, resTask3);

// Now you can retrieve the results
var res1 = await resTask1;
var res2 = await resTask2;
var res3 = await resTask3;
Adalyat Nazirov
  • 1,611
  • 2
  • 14
  • 28
  • Directly using the `Result` property is ugly, misses the point of the abstraction, and more importantly changes exception propagation dramatically. Just use `var results = await Task.WhenAll(resTask1, resTask2, resTask3);` – Aluan Haddad Jun 07 '23 at 00:56
  • @AluanHaddad Right, but`var results = await ...` won't work if individual task values are of different types. see details https://stackoverflow.com/a/17197786/2091519 – Adalyat Nazirov Jun 07 '23 at 06:46
  • 1
    That's a very important point but if you look at the answers in that very thread you'll find the issue of exception propagation is addressed and this is necessary. See https://stackoverflow.com/a/17197786/1915893 and https://stackoverflow.com/a/17197745/1915893 – Aluan Haddad Jun 07 '23 at 09:17