16

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?

  • 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 6 '17 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 Jan 6 '17 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 Jan 6 '17 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 Jan 6 '17 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. – Ankit Vijay Jan 6 '17 at 5:21
16

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;
  • 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
  • 3
    @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 3 at 22:18
  • @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 4 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 5 at 1:33
0

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();

protected by gnat May 10 '18 at 6:20

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

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