Вызов нескольких асинхронных служб параллельно

17

У меня есть несколько асинхронных служб REST, которые не зависят друг от друга. То есть, пока «ожидая» ответа от Service1, я могу позвонить в Service2, Service3 и так далее.

Например, смотрите ниже код:

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

// Use service1Response and service2Response

Теперь service2Responseне зависит, service1Responseи они могут быть получены независимо. Следовательно, мне не нужно ждать ответа первой службы для вызова второй службы.

Я не думаю, что я могу использовать Parallel.ForEachздесь, поскольку это не связано с процессором операции.

Можно ли вызвать эти две операции параллельно, могу ли я вызвать use Task.WhenAll? Одна проблема, которую я вижу, Task.WhenAllэто то, что она не возвращает результаты. Чтобы получить результат, я могу позвонить task.Resultпосле вызова Task.WhenAll, так как все задачи уже выполнены, и все, что мне нужно, чтобы получить нам ответ?

Образец кода:

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

await Task.WhenAll(task1, task2)

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

// Use result1 and result2

Этот код лучше, чем первый, с точки зрения производительности? Любой другой подход, который я могу использовать?

Анкит Виджай
источник
I do not think I can use Parallel.ForEach here since it is not CPU bound operation- Я не вижу логики там. Параллелизм - это параллелизм.
Роберт Харви
3
@RobertHarvey Я предполагаю, что проблема в том, что в этом контексте Parallel.ForEachбудут порождаться новые потоки, тогда как async awaitвсе будет выполняться в одном потоке.
MetaFight
@ Анкит зависит от того, когда уместно блокировать твой код. Ваш второй пример будет блокироваться, пока оба ответа не будут готовы. Ваш первый пример, предположительно, будет логически блокироваться только тогда, когда код попытается использовать response ( await), прежде чем он будет готов.
MetaFight
Возможно, будет проще дать вам более удовлетворительный ответ, если вы предоставите менее абстрактный пример кода, использующего оба ответа службы.
MetaFight
@MetaFight Во втором примере, который я делаю, я делаю WhenAllперед тем, как делаю Resultмысль, что он завершает все задачи перед вызовом .Result. Поскольку Task.Result блокирует вызывающий поток, я предполагаю, что если я вызову его после фактического завершения задач, он немедленно вернет результат. Я хочу подтвердить понимание.
Анкит Виджай

Ответы:

17

Одна проблема, которую я вижу при использовании Task.WhenAll, заключается в том, что он не возвращает результаты

Но это действительно возвращает результаты. Все они будут в массиве общего типа, поэтому не всегда полезно использовать результаты, так как вам нужно найти в массиве элемент, соответствующий тому, для Taskкоторого вы хотите получить результат, и потенциально привести его к фактический тип, так что это может быть не самый простой / наиболее читаемый подход в этом контексте, но когда вы просто хотите получить все результаты из каждой задачи, а общий тип - это тип, к которому вы хотите относиться к ним, тогда это здорово ,

Чтобы получить результат, я могу вызвать task.Result после вызова Task.WhenAll, так как все задачи уже выполнены и все, что мне нужно для получения ответа от нас?

Да, вы могли бы сделать это. Вы также можете awaitих использовать ( awaitразворачивать исключение в любой ошибочной задаче, тогда как Resultгенерировало бы исключение агрегирования, но в противном случае оно было бы таким же).

Этот код лучше, чем первый, с точки зрения производительности?

Он выполняет две операции одновременно, а не одну, а затем другую. Будет ли это лучше или хуже, зависит от того, каковы эти основные операции. Если основными операциями являются «чтение файла с диска», то параллельное выполнение их, вероятно, будет медленнее, поскольку имеется только одна головка диска и она может быть только в одном месте в любой момент времени; прыжок между двумя файлами будет медленнее, чем чтение одного файла, затем другого. С другой стороны, если операции «выполняют какой-то сетевой запрос» (как в данном случае), то они, скорее всего, будут быстрее (по крайней мере, до определенного числа одновременных запросов), потому что вы можете ждать ответа с какого-либо другого сетевого компьютера так же быстро, когда происходит также другой ожидающий сетевой запрос. Если вы хотите знать, если это

Любой другой подход, который я могу использовать?

Если для вас не важно, что вы знаете все исключения, сгенерированные среди всех операций, которые вы выполняете параллельно, а не только первую, вы можете просто awaitвыполнять задачи WhenAllвообще. Единственное, что WhenAllдает вам - это иметь AggregateExceptionс каждым исключением из каждой ошибочной задачи, а не бросать, когда вы нажимаете первую неисправную задачу. Это так просто, как:

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

var result1 = await task1;
var result2 = await task2;
Servy
источник
Это не выполнение задач одновременно, не говоря уже о параллельном. Вы ждете, когда каждая задача будет выполнена в последовательном порядке. Вполне нормально, если вас не интересует исходный код.
Рик О'Ши
3
@ RickO'Shea Это начинает операции последовательно. Он начнет вторую операцию после того, как * начнет первую операцию. Но запуск асинхронной операции должен быть в основном мгновенным (если это не так, на самом деле он не асинхронный, и это ошибка в этом методе). После запуска одного, а затем другого он не будет продолжаться до тех пор, пока не закончится первое, а затем второе. Поскольку ничто не ждет завершения первого, прежде чем запустить второе, ничто не мешает им работать одновременно (что аналогично тому, как они работают параллельно).
Servy
@ Служу, я не думаю, что это правда. Я добавил ведение журнала внутри двух асинхронных операций, каждая из которых занимала около одной секунды (обе выполняют http-вызовы), а затем вызывал их, как вы предложили, и, конечно же, задача1 запускалась и заканчивалась, а затем запускалась и заканчивалась.
Мэтт Фреар
@MattFrear Тогда метод фактически не был асинхронным. Это было синхронно. По определению , асинхронный метод будет возвращаться сразу, а не после того, как операция фактически завершится.
Servy
@Servy по определению, ожидание будет означать, что вы дождетесь завершения асинхронной задачи перед выполнением следующей строки. Не так ли?
Мэтт Фрир
0

Вот метод расширения, который использует SemaphoreSlim и позволяет установить максимальную степень параллелизма

    /// <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)));
        }
    }

Пример использования:

await enumerable.ForEachAsyncConcurrent(
    async item =>
    {
        await SomeAsyncMethod(item);
    },
    5);
Джей шах
источник
-2

Вы можете использовать

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

или

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
источник
Почему понижает?
user1451111