22

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?

11
  • 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. Commented Jan 6, 2017 at 3: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
    Commented Jan 6, 2017 at 4: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
    Commented Jan 6, 2017 at 4: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
    Commented Jan 6, 2017 at 4: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. Commented Jan 6, 2017 at 5:21

3 Answers 3

20

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;
9
  • 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. Commented Sep 21, 2018 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
    Commented Sep 21, 2018 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
    Commented Apr 3, 2019 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
    Commented Apr 4, 2019 at 13:21
  • 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
    Commented Jan 31, 2020 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);
-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();
2
  • Why downvotes ? Commented Oct 25, 2018 at 23:52
  • This does not ensure both tasks are done.
    – nivs1978
    Commented Aug 19 at 11:59

Not the answer you're looking for? Browse other questions tagged or ask your own question.