Chiamata di più servizi asincroni in parallelo


17

Ho pochi servizi REST asincroni che non dipendono l'uno dall'altro. Cioè mentre "aspetto" una risposta da Service1, posso chiamare Service2, Service3 e così via.

Ad esempio, consultare il codice seguente:

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

// Use service1Response and service2Response

Ora, service2Responsenon dipende da service1Responsee possono essere recuperati in modo indipendente. Quindi, non è necessario che attenda la risposta del primo servizio per chiamare il secondo servizio.

Non credo di poter usare Parallel.ForEachqui poiché non è un'operazione legata alla CPU.

Per chiamare queste due operazioni in parallelo, posso chiamare use Task.WhenAll? Un problema che vedo usando Task.WhenAllè che non restituisce risultati. Per recuperare il risultato posso chiamare task.Resultdopo aver chiamato Task.WhenAll, dal momento che tutte le attività sono già state completate e tutto ciò di cui ho bisogno per ricevere risposta?

Codice d'esempio:

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

await Task.WhenAll(task1, task2)

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

// Use result1 and result2

Questo codice è migliore del primo in termini di prestazioni? Qualche altro approccio che posso usare?


I do not think I can use Parallel.ForEach here since it is not CPU bound operation- Non vedo la logica lì. La concorrenza è concorrenza.
Robert Harvey,

3
@RobertHarvey Immagino che la preoccupazione sia che, in questo contesto, Parallel.ForEachgenerino nuovi thread mentre async awaitfarebbero tutto su un singolo thread.
MetaFight,

@Ankit dipende da quando è appropriato bloccare il codice. Il tuo secondo esempio verrebbe bloccato fino a quando entrambe le risposte non saranno pronte. Il tuo primo esempio, presumibilmente, si bloccherebbe logicamente solo quando il codice tenterà di usare la risposta ( await) prima che sia pronto.
MetaFight,

Potrebbe essere più semplice fornirti una risposta più soddisfacente se fornissi un esempio meno astratto del codice che utilizza entrambe le risposte del servizio.
MetaFight,

@MetaFight Nel mio secondo esempio sto facendo WhenAllprima di fare Resultcon l'idea che completa tutte le attività prima che venga chiamato .Result. Poiché Task.Result blocca il thread chiamante, presumo che se lo chiamo dopo che le attività sono state effettivamente completate, restituirebbe immediatamente il risultato. Voglio convalidare la comprensione.
Ankit Vijay,

Risposte:


17

Un problema che vedo usando Task.WhenAll è che non restituisce risultati

Ma fa restituire i risultati. Saranno tutti in una matrice di un tipo comune, quindi non è sempre utile usare i risultati in quanto è necessario trovare la voce nella matrice che corrisponde a quella per Taskcui si desidera il risultato e potenzialmente lanciarlo nella sua tipo reale, quindi potrebbe non essere l'approccio più semplice / più leggibile in questo contesto, ma quando vuoi solo avere tutti i risultati di ogni attività, e il tipo comune è il tipo con cui vuoi trattarli, quindi è fantastico .

Per recuperare il risultato posso chiamare task.Result dopo aver chiamato Task.WhenAll, poiché tutte le attività sono già state completate e tutto ciò di cui ho bisogno per recuperare la risposta?

Sì, potresti farlo. Potresti anche awaitloro ( awaitscartare l'eccezione in qualsiasi attività difettosa, mentre Resultlancerebbe un'eccezione aggregata, ma altrimenti sarebbe la stessa).

Questo codice è migliore del primo in termini di prestazioni?

Esegue le due operazioni contemporaneamente, anziché l'una e poi l'altra. Che sia meglio o peggio dipende da quali sono le operazioni sottostanti. Se le operazioni sottostanti sono "leggere un file dal disco", probabilmente è più lento eseguirle in parallelo, poiché esiste solo una testa del disco e può trovarsi in un solo posto in un dato momento; saltare tra due file sarà più lento della lettura di un file poi di un altro. D'altra parte, se le operazioni "eseguono alcune richieste di rete" (come nel caso qui), molto probabilmente saranno più veloci (almeno fino a un certo numero di richieste simultanee), perché puoi attendere una risposta da qualche altro computer di rete altrettanto velocemente quando c'è anche qualche altra richiesta di rete in sospeso. Se vuoi sapere se '

Qualche altro approccio che posso usare?

Se per te non è importante che tu conosca tutte le eccezioni lanciate tra tutte le operazioni che stai facendo in parallelo piuttosto che solo la prima, puoi semplicemente awaitsvolgere le attività senza WhenAllaffatto. L'unica cosa che WhenAllti dà è avere AggregateExceptionun'eccezione con ogni singola eccezione da ogni attività con errori, piuttosto che lanciare quando si colpisce la prima attività con errore. È semplice come:

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

var result1 = await task1;
var result2 = await task2;

Questo non sta eseguendo attività contemporaneamente e tanto meno in parallelo. Stai aspettando che ogni attività venga completata in ordine sequenziale. Completamente bene se non ti interessa il codice performante.
Rick O'Shea,

3
@ RickO'Shea Inizia le operazioni in sequenza. Si avvia la seconda operazione dopo che * avvia la prima operazione. Ma l' avvio dell'operazione asincrona dovrebbe essere sostanzialmente istantaneo (se non lo è, non è in realtà asincrono, e questo è un bug in quel metodo). Dopo aver avviato uno, e poi l'altro, non continuerà fino a quando non termina il primo, quindi termina il secondo. Poiché nulla attende che il primo termini prima di iniziare il secondo, nulla impedisce loro di funzionare contemporaneamente (che è lo stesso di loro che corrono in parallelo).
Servito il

@Servy Non penso sia vero. Ho aggiunto la registrazione all'interno di due operazioni asincrone che hanno richiesto circa un secondo ciascuna (entrambe le chiamate http) e poi le ho chiamate come hai suggerito, e sicuramente un'attività1 iniziata e terminata e poi attività2 avviata e terminata.
Matt Frear,

@MattFrear Quindi il metodo non era in realtà asincrono. Era sincrono. Per definizione , un metodo asincrono tornerà immediatamente, anziché tornare dopo che l'operazione è stata effettivamente completata.
Servito il

@Servy per definizione, l'attesa significherà aspettare fino al termine dell'attività asincrona prima di eseguire la riga successiva. No?
Matt Frear,

0

Ecco il metodo di estensione che utilizza SemaphoreSlim e consente di impostare il massimo grado di parallelismo

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

Esempio di utilizzo:

await enumerable.ForEachAsyncConcurrent(
    async item =>
    {
        await SomeAsyncMethod(item);
    },
    5);

-2

Puoi usare entrambi

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

o

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

Perché i voti negativi?
user1451111,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.