21

I have few async REST services which are not dependent on each other. That is while "awaiting" a response from Service1, I can call Service2, Service3 and so on.

For example, refer below code:

var service1Response = await HttpService1Async();
var service2Response = await HttpService2Async();

// Use service1Response and service2Response

Now, service2Response is not dependent on service1Response and they can be fetched independently. Hence, there is no need for me to await response of first service to call the second service.

I do not think I can use Parallel.ForEach here since it is not CPU bound operation.

In order to call these two operations in parallel, can I call use Task.WhenAll? One issue I see using Task.WhenAll is that it does not return results. To fetch the result can I call task.Result after calling Task.WhenAll, since all tasks are already completed and all I need to fetch us response?

Sample Code:

var task1 = HttpService1Async();
var task2 = HttpService2Async();

await Task.WhenAll(task1, task2)

var result1 = task1.Result;
var result2 = task2.Result;

// Use result1 and result2

Is this code better than the first one in terms of performance? Any other approach I can use?

DasJacko
  • 3
  • 2
Ankit Vijay
  • 1,568
  • 3
  • 10
  • 13
  • `I do not think I can use Parallel.ForEach here since it is not CPU bound operation` -- I don't see the logic there. Concurrency is concurrency. – Robert Harvey Jan 06 '17 at 03:11
  • 3
    @RobertHarvey I'm guessing the concern is that, in this context, `Parallel.ForEach` would spawn new threads whereas `async await` would do everything on a single thread. – MetaFight Jan 06 '17 at 04:01
  • @Ankit it depends one when it's appropriate for your code to block. Your second example would block until both responses are ready. Your first example, presumably, would only logically block when the code would attempt to use the response (`await`) before it is ready. – MetaFight Jan 06 '17 at 04:05
  • It might be easier to give you a more satisfying answer if you provided a less abstract example of the code consuming both service responses. – MetaFight Jan 06 '17 at 04:06
  • @MetaFight In my second example I'm doing `WhenAll` before I do `Result` with the idea that it completes all the tasks before .Result is called. Since, Task.Result blocks the calling thread, I presume that if I call it after the tasks are actually completed it would return thre result immediately. I want to validate the understanding. – Ankit Vijay Jan 06 '17 at 05:21
  • Yes, if you `Task.WhenAll(...)` first then your `task.Result` should return immediately. If you do this, however, you're not really leveraging `async await` though. – MetaFight Jan 06 '17 at 05:31
  • I'm still doing an await on WhenAll. So, this way I understand I'm able to use the best of both world. Right? – Ankit Vijay Jan 06 '17 at 10:51
  • @MetaFight Not even a single thread, it *wouldn't use any threads at all*. The one thread would start the operations and then *would be free to go off doing other work* while these requests are going on, meaning that these requests are consuming *no* thread's time, rather than even a single thread. – Servy Jan 06 '17 at 16:43
  • @MetaFight Sure they are. Since you `await` the call to `WhenAll` the method is asynchronous. Whether you `await` or call `Result` on the *already completed* `Task` isn't that different. They have slightly different error handling behaviors, but that's about it. – Servy Jan 07 '17 at 21:08
  • @Servy, I didn't say *no threads* were used. I said no *new threads* were *spawned*. Also, using `await` with `Task.WhenAll()` is hardly leveraging `async await`. Doing something like `var result1 = service1.GetFooAsync(); doOtherStuff(); var result2 = service2.GetBarAsync(); var baz = doMoreStuff(await result1); return mix(baz, await result2);` is making better use of the feature. The `async await` allows async code while still keeping it grouped logically. All the steps in your algorithm will still appear sequentially, even though blocking and continuation magic happens automatically. – MetaFight Jan 08 '17 at 00:15
  • @MetaFight If awaiting results in an IO bound operation, the await immediately frees the thread to do other work, so even the simple example is making good using of async/await. – Andy Oct 22 '18 at 23:15

3 Answers3

19

One issue I see using Task.WhenAll is that it does not return results

But it does return the results. They'll all be in an array of a common type, so it's not always useful to use the results in that you need to find the item in the array that corresponds to the Task that you want the result for, and potentially cast it to its actual type, so it might not be the easiest/most readable approach in this context, but when you just want to have all of the results from every task, and the common type is the type you want to treat them as, then it's great.

To fetch the result can I call task.Result after calling Task.WhenAll, since all tasks are already completed and all I need to fetch us response?

Yes, you could do that. You could also await them (await would unwrap the exception in any faulted task, whereas Result would throw an aggregate exception, but otherwise it'd be the same).

Is this code better than the first one in terms of performance?

It performs the two operations at the same time, rather than one and then the other. Whether that's better or worse is dependant on what those underlying operations are. If the underlying operations are "read a file from disk" then doing them in parallel is likely slower, as there is only one disk head and it can only be in one place at any given time; it jumping around between two files will be slower than reading one file then another. On the other hand, if the operations are "perform some network request" (as is the case here) then they'll very likely be faster (at least up to a certain number of concurrent requests), because you can wait for a response from some other network computer just as fast when there is also some other pending network request going on. If you want to know if it's faster in your situation, test it.

Any other approach I can use?

If it's not important to you that you know all of the exceptions thrown among all of the operations you're doing in parallel rather than just the first one, you can simply await the tasks without WhenAll at all. The only thing WhenAll gives you is having an AggregateException with every single exception from every faulted task, rather than throwing when you hit the first faulted task. It's as simple as:

var task1 = HttpService1Async();
var task2 = HttpService2Async();

var result1 = await task1;
var result2 = await task2;
Servy
  • 1,966
  • 14
  • 12
  • 3
    That's not running tasks in concurrently let alone in parallel. You're waiting for each task to complete in sequential order. Completely fine if you don't care about performant code. – Rick O'Shea Sep 21 '18 at 18:38
  • 4
    @RickO'Shea It *starts* the operations sequentially. It will *start the second operation after it *starts* the first operation. But *starting* the asynchronous operation should be basically instantaneous (if it's not, it's not actually asynchronous, and that's a bug in that method). After starting one, and then the other, it won't continue on until after the first finishes, and then the second finishes. Since nothing waits for the first to finish before starting the second, nothing is stopping them from running concurrently (which is the same as them running in parallel). – Servy Sep 21 '18 at 20:54
  • @Servy I don't think that is true. I added logging inside two async operations which took around one second each (both make http calls) and then called them as you have suggested, and sure enough task1 started and ended and then task2 started and ended. – Matt Frear Apr 03 '19 at 22:18
  • 1
    @MattFrear Then the method was not in fact asynchronous. It was synchronous. *By definition*, an asynchronous method is going to return right away, rather than returning after the operation has actually finished. – Servy Apr 04 '19 at 13:21
  • @Servy by definition, the await will mean you wait until the asyncronous task finishes before executing the next line. Isn't it? – Matt Frear Apr 05 '19 at 01:33
  • @MattFrear No. `await` composes asynchronous operations by providing a more convenient syntax for adding continuations to tasks, it does not synchronously block. You don't need a language feature to synchronously block on an asynchronous operation. Additionally, as mentioned in the above comment, both asynchronous operations are started before awaiting either, assuming the operations are in fact asynchronous. – Servy Apr 05 '19 at 13:19
  • @Servy you know more about this than me so thanks for attempting to explain it. At least with my use case, calling await twice had different results than Task.WhenAll(task1, task2) as proven by my logging. FWIW I was calling ServiceStack.Text's string.GetJsonFromUrlAsync() method to make the async calls... with lots of async and await through all the layers of my application code in between. – Matt Frear Apr 05 '19 at 16:54
  • [This experiment](https://jeremylindsayni.wordpress.com/2019/03/11/using-async-await-and-task-whenall-to-improve-the-overall-speed-of-your-c-code/) would seem to bolster Rick O'Shea's case. You should use `Task.WhenAll()`, otherwise it would seem they are not run in parallel. Multiple `await`s in a row may not block the _caller_, but they _do wait until the `Task` completes before proceeding_ (e.g., before executing the next `await` or logging a message between two `await`s, as Matt Frear observed). – System.Cats.Lol Jan 31 '20 at 15:26
  • 2
    @System.Cats.Lol That post is just full of bad information. As to this one point, they compare code that starts one operation, awaits it, then starts the next operation, then awaits that one, whereas the code I showed starts *all* operations, and *then* awaits them all. That article also uses the word "synchronous" all over to refer to operations that are in fact *asynchronous* (what they should have said was that the first snippet performed the operations *sequentially*, not synchronously), and uses the term "asynchronous" to refer to things that are happening in parallel. – Servy Jan 31 '20 at 22:07
1

Here's the extension method which makes use of SemaphoreSlim and allows to set maximum degree of parallelism

    /// <summary>
    /// Concurrently Executes async actions for each item of <see cref="IEnumerable<typeparamref name="T"/>
    /// </summary>
    /// <typeparam name="T">Type of IEnumerable</typeparam>
    /// <param name="enumerable">instance of <see cref="IEnumerable<typeparamref name="T"/>"/></param>
    /// <param name="action">an async <see cref="Action" /> to execute</param>
    /// <param name="maxDegreeOfParallelism">Optional, An integer that represents the maximum degree of parallelism,
    /// Must be grater than 0</param>
    /// <returns>A Task representing an async operation</returns>
    /// <exception cref="ArgumentOutOfRangeException">If the maxActionsToRunInParallel is less than 1</exception>
    public static async Task ForEachAsyncConcurrent<T>(
        this IEnumerable<T> enumerable,
        Func<T, Task> action,
        int? maxDegreeOfParallelism = null)
    {
        if (maxDegreeOfParallelism.HasValue)
        {
            using (var semaphoreSlim = new SemaphoreSlim(
                maxDegreeOfParallelism.Value, maxDegreeOfParallelism.Value))
            {
                var tasksWithThrottler = new List<Task>();

                foreach (var item in enumerable)
                {
                    // Increment the number of currently running tasks and wait if they are more than limit.
                    await semaphoreSlim.WaitAsync();

                    tasksWithThrottler.Add(Task.Run(async () =>
                    {
                        await action(item).ContinueWith(res =>
                        {
                            // action is completed, so decrement the number of currently running tasks
                            semaphoreSlim.Release();
                        });
                    }));
                }

                // Wait for all tasks to complete.
                await Task.WhenAll(tasksWithThrottler.ToArray());
            }
        }
        else
        {
            await Task.WhenAll(enumerable.Select(item => action(item)));
        }
    }

Sample Usage:

await enumerable.ForEachAsyncConcurrent(
    async item =>
    {
        await SomeAsyncMethod(item);
    },
    5);
Jay Shah
  • 111
  • 3
-2

You can either use

Parallel.Invoke(() =>
{
    HttpService1Async();
},
() =>
{   
    HttpService2Async();
});

or

Task task1 = Task.Run(() => HttpService1Async());
Task task2 = Task.Run(() => HttpService2Async());

//If you wish, you can wait for a particular task to return here like this:
task1.Wait();
user1451111
  • 219
  • 2
  • 5