Wywoływanie wielu usług asynchronicznych jednocześnie

17

Mam kilka asynchronicznych usług REST, które nie są od siebie zależne. To znaczy, „czekając” na odpowiedź Service1, mogę zadzwonić do Service2, Service3 i tak dalej.

Na przykład zapoznaj się z poniższym kodem:

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

// Use service1Response and service2Response

Teraz service2Responsenie zależy od nich service1Responsei można je pobrać niezależnie. Dlatego nie muszę czekać na odpowiedź pierwszej usługi, aby zadzwonić do drugiej usługi.

Nie sądzę, żebym mógł Parallel.ForEachtutaj użyć , ponieważ nie jest to operacja związana z procesorem.

Czy mogę wywołać te dwie operacje równolegle, czy mogę wywołać use Task.WhenAll? Jednym z problemów, z których korzystam, Task.WhenAlljest to, że nie zwraca wyników. Aby pobrać wynik, czy mogę zadzwonić task.Resultpo wywołaniu Task.WhenAll, ponieważ wszystkie zadania są już zakończone i wystarczy pobrać odpowiedź?

Przykładowy kod:

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

await Task.WhenAll(task1, task2)

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

// Use result1 and result2

Czy ten kod jest lepszy od pierwszego pod względem wydajności? Jakieś inne podejście, którego mogę użyć?

Ankit Vijay
źródło
I do not think I can use Parallel.ForEach here since it is not CPU bound operation- Nie widzę tam logiki. Współbieżność to współbieżność.
Robert Harvey
3
@RobertHarvey Zgaduję, że problemem jest to, że w tym kontekście Parallel.ForEachpojawiłyby się nowe wątki, podczas async awaitgdy wszystko robiłoby w jednym wątku.
MetaFight,
@Załóż to zależy od tego, kiedy kod jest odpowiedni do zablokowania. Drugi przykład zostanie zablokowany, dopóki obie odpowiedzi nie będą gotowe. Twój pierwszy przykład prawdopodobnie logicznie blokuje się tylko wtedy, gdy kod będzie próbował użyć odpowiedzi ( await), zanim będzie gotowy.
MetaFight,
Udzielenie bardziej satysfakcjonującej odpowiedzi może być łatwiejsze, jeśli podasz mniej abstrakcyjny przykład kodu wykorzystującego obie odpowiedzi usługi.
MetaFight,
@MetaFight W moim drugim przykładzie robię, WhenAllzanim skończę Resultz pomysłem, że wykona wszystkie zadania przed wywołaniem funkcji .Result. Ponieważ Task.Result blokuje wątek wywołujący, zakładam, że jeśli wywołam go po zakończeniu zadań, natychmiast zwróci wynik. Chcę potwierdzić zrozumienie.
Ankit Vijay

Odpowiedzi:

17

Jeden problem, który widzę przy użyciu Task.WhenAll polega na tym, że nie zwraca wyników

Ale to nie zwróci wyniki. Wszystkie będą w tablicy tego samego typu, więc nie zawsze przydatne jest użycie wyników, w których musisz znaleźć element w tablicy, który odpowiada Taskwynikowi, dla którego chcesz uzyskać wynik, i potencjalnie rzuć go na jego rzeczywisty typ, więc może nie być to najłatwiejsze / najbardziej czytelne podejście w tym kontekście, ale jeśli chcesz mieć wszystkie wyniki z każdego zadania, a typowym typem jest typ, którym chcesz je traktować, to świetnie .

Aby pobrać wynik, mogę wywołać task.Result po wywołaniu Task.WhenAll, ponieważ wszystkie zadania są już ukończone i wszystko, czego potrzebuję, aby pobrać odpowiedź?

Tak, możesz to zrobić. Mógłbyś także awaitje ( awaitrozpakowałby wyjątek w każdym zadaniu z błędem, natomiast Resultwygenerowałby wyjątek zbiorczy, ale w przeciwnym razie byłby taki sam).

Czy ten kod jest lepszy od pierwszego pod względem wydajności?

Wykonuje dwie operacje jednocześnie, zamiast jednej, a potem drugiej. To, czy jest to lepsze czy gorsze, zależy od tego, jakie są te operacje leżące u podstaw. Jeśli podstawowymi operacjami są „odczytywanie pliku z dysku”, wykonywanie ich równolegle jest prawdopodobnie wolniejsze, ponieważ istnieje tylko jedna głowica dysku i może znajdować się tylko w jednym miejscu w danym momencie; przeskakiwanie między dwoma plikami będzie wolniejsze niż czytanie jednego pliku po drugim. Z drugiej strony, jeśli operacje będą „wykonywać niektóre żądania sieciowe” (jak ma to miejsce w tym przypadku), prawdopodobnie będą one szybsze (przynajmniej do pewnej liczby równoczesnych żądań), ponieważ możesz poczekać na odpowiedź z innego komputera sieciowego tak samo szybko, gdy trwa również inne oczekujące żądanie sieciowe. Jeśli chcesz wiedzieć, czy to ”

Jakieś inne podejście, którego mogę użyć?

Jeśli nie jest dla Ciebie ważne, że znasz wszystkie wyjątki zgłoszone między wszystkimi operacjami wykonywanymi równolegle, a nie tylko pierwszą, możesz po prostu wykonać awaitzadania WhenAllw ogóle. Jedyną rzeczą, jaką WhenAlldajesz, jest posiadanie AggregateExceptionkażdego wyjątku od każdego błędnego zadania, zamiast rzucania, gdy trafisz pierwsze błędne zadanie. To tak proste, jak:

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

var result1 = await task1;
var result2 = await task2;
Servy
źródło
To nie uruchamia zadań jednocześnie, a tym bardziej równolegle. Czekasz na wykonanie każdego zadania w kolejności sekwencyjnej. Zupełnie dobrze, jeśli nie zależy ci na wydajnym kodzie.
Rick O'Shea,
3
@ RickO'Shea Rozpoczyna operacje sekwencyjnie. Będzie ona uruchomić drugą operację po * rozpoczyna pierwszą operację. Ale rozpoczęcie operacji asynchronicznej powinno być w zasadzie natychmiastowe (jeśli nie jest, to nie jest tak naprawdę asynchroniczne, a to błąd w tej metodzie). Po uruchomieniu jednego, a następnie drugiego, nie będzie on kontynuowany, dopóki nie zakończy się pierwszy, a następnie drugi. Ponieważ nic nie czeka na zakończenie pierwszego przed rozpoczęciem drugiego, nic nie powstrzymuje ich przed jednoczesnym uruchomieniem (co jest tym samym, co równoległe).
Servy
@Servy Nie sądzę, że to prawda. Dodałem rejestrowanie w dwóch operacjach asynchronicznych, które trwały około jednej sekundy każda (obie wykonują wywołania HTTP), a następnie wywoływałem je zgodnie z sugestią i upewniłem się, że wystarczająca liczba zadań 1 została uruchomiona i zakończona, a następnie zadanie 2 uruchomione i zakończone.
Matt Frear,
@MattFrear Wtedy metoda nie była w rzeczywistości asynchroniczna. To było synchroniczne. Z definicji metoda asynchroniczna powróci natychmiast, a nie po zakończeniu operacji.
Servy
@Servy z definicji, oczekiwanie będzie oznaczało, że przed wykonaniem następnego wiersza zaczekaj, aż asynchroniczne zadanie zakończy się. Czyż nie
Matt Frear,
0

Oto metoda rozszerzenia, która wykorzystuje SemaphoreSlim i pozwala ustawić maksymalny stopień równoległości

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

Przykładowe użycie:

await enumerable.ForEachAsyncConcurrent(
    async item =>
    {
        await SomeAsyncMethod(item);
    },
    5);
Jay Shah
źródło
-2

Możesz albo użyć

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

lub

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();
użytkownik1451111
źródło
Dlaczego głosować negatywnie?
user1451111,