Chamando vários serviços assíncronos em paralelo

17

Eu tenho poucos serviços REST assíncronos que não são dependentes um do outro. Ou seja, enquanto "aguarda" uma resposta do Service1, posso chamar Service2, Service3 e assim por diante.

Por exemplo, consulte o código abaixo:

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

// Use service1Response and service2Response

Agora, service2Responsenão depende service1Responsee eles podem ser buscados independentemente. Portanto, não há necessidade de aguardar a resposta do primeiro serviço para chamar o segundo serviço.

Eu não acho que posso usar Parallel.ForEachaqui, pois não é uma operação vinculada à CPU.

Para chamar essas duas operações em paralelo, posso chamar use Task.WhenAll? Um problema que vejo usando Task.WhenAllé que ele não retorna resultados. Para buscar o resultado, posso ligar task.Resultdepois de ligar Task.WhenAll, pois todas as tarefas já estão concluídas e tudo o que preciso para nos buscar uma resposta?

Código de amostra:

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

await Task.WhenAll(task1, task2)

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

// Use result1 and result2

Esse código é melhor que o primeiro em termos de desempenho? Alguma outra abordagem que eu possa usar?

Ankit Vijay
fonte
I do not think I can use Parallel.ForEach here since it is not CPU bound operation- Eu não vejo a lógica lá. Simultaneidade é simultaneidade.
Robert Harvey
3
@RobertHarvey Acho que a preocupação é que, nesse contexto, Parallel.ForEachgeraria novos threads, enquanto async awaitfaria tudo em um único thread.
MetaFight
O @Ankit depende de um quando é apropriado para o seu código bloquear. Seu segundo exemplo seria bloqueado até as duas respostas estarem prontas. Seu primeiro exemplo, presumivelmente, só bloqueará logicamente quando o código tentará usar a resposta ( await) antes de estar pronta.
MetaFight
Pode ser mais fácil fornecer uma resposta mais satisfatória se você forneceu um exemplo menos abstrato do código que consome as duas respostas de serviço.
MetaFight
@MetaFight No meu segundo exemplo, estou fazendo WhenAllantes de Resultconcluir a ideia de que ele conclui todas as tarefas antes de .Result ser chamado. Como o Task.Result bloqueia o segmento de chamada, presumo que, se eu chamá-lo após a conclusão das tarefas, retornará o resultado imediatamente. Eu quero validar o entendimento.
Ankit Vijay

Respostas:

17

Um problema que eu vejo usando o Task.WhenAll é que ele não retorna resultados

Mas faz retornar os resultados. Todos eles estarão em uma matriz de um tipo comum; portanto, nem sempre é útil usar os resultados, pois você precisa encontrar o item na matriz que corresponde ao valor para o Taskqual deseja o resultado e potencialmente convertê-lo em sua tipo real, portanto, pode não ser a abordagem mais fácil / mais legível nesse contexto, mas quando você deseja obter todos os resultados de cada tarefa, e o tipo comum é o tipo em que você deseja tratá-los, é ótimo .

Para buscar o resultado, posso chamar task.Result depois de chamar Task.WhenAll, já que todas as tarefas já foram concluídas e tudo o que preciso para nos buscar resposta?

Sim, você poderia fazer isso. Você também pode awaitusá-los ( awaitdesembrulharia a exceção em qualquer tarefa com falha, ao passo Resultque lançaria uma exceção agregada, mas, caso contrário, seria a mesma).

Esse código é melhor que o primeiro em termos de desempenho?

Ele executa as duas operações ao mesmo tempo, em vez de uma e depois a outra. Se isso é melhor ou pior, depende do que são essas operações subjacentes. Se as operações subjacentes forem "ler um arquivo do disco", é provável que fazê-las em paralelo seja mais lento, pois há apenas uma cabeça de disco e ela pode estar em um local a qualquer momento; saltar entre dois arquivos será mais lento do que ler um arquivo e depois outro. Por outro lado, se as operações "executam algumas solicitações de rede" (como é o caso aqui), provavelmente serão mais rápidas (pelo menos até um certo número de solicitações simultâneas), porque você pode esperar por uma resposta de outro computador da rede com a mesma rapidez quando houver outra solicitação de rede pendente em andamento. Se você quer saber se '

Alguma outra abordagem que eu possa usar?

Se não for importante para você conhecer todas as exceções lançadas entre todas as operações que você está executando em paralelo, em vez de apenas a primeira, você pode simplesmente awaitexecutar as tarefas sem WhenAllabsolutamente nenhuma . A única coisa que WhenAllvocê oferece é ter AggregateExceptiontodas as exceções de todas as tarefas com falha, em vez de lançar quando você atingir a primeira tarefa com falha. É tão simples quanto:

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

var result1 = await task1;
var result2 = await task2;
Servy
fonte
Isso não está executando tarefas simultaneamente e muito menos em paralelo. Você está aguardando a conclusão de cada tarefa em ordem seqüencial. Completamente bem se você não se importa com o código de desempenho.
Rick O'Shea
3
@ RickO'Shea Inicia as operações sequencialmente. Ele iniciará a segunda operação após * iniciar a primeira operação. Mas iniciar a operação assíncrona deve ser basicamente instantâneo (se não for, não é realmente assíncrono, e isso é um bug nesse método). Depois de iniciar um e depois o outro, ele não continuará até depois do primeiro término e depois do segundo. Como nada espera que o primeiro termine antes de iniciar o segundo, nada os impede de executar simultaneamente (que é o mesmo que eles executando em paralelo).
Servy
@ Service Eu não acho que isso é verdade. Adicionei o log dentro de duas operações assíncronas que levavam cerca de um segundo cada (ambas fazem chamadas http) e as chamei como sugerido, e com certeza a tarefa1 foi iniciada e finalizada e a tarefa2 foi iniciada e finalizada.
Matt Frear 03/04
@ MattFrear Então o método não era de fato assíncrono. Foi síncrono. Por definição , um método assíncrono retornará imediatamente, em vez de retornar após a conclusão da operação.
Servy 04/04
@Servy por definição, a espera significa que você espera até que a tarefa assíncrona termine antes de executar a próxima linha. Não é?
Matt Frear 5/04
0

Aqui está o método de extensão que utiliza o SemaphoreSlim e permite definir o grau máximo de paralelismo

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

Uso da amostra:

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

Você pode usar

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

ou

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
fonte
Por que votos negativos?
user1451111