Mehrere asynchrone Dienste gleichzeitig aufrufen

17

Ich habe nur wenige asynchrone REST-Services, die nicht voneinander abhängig sind. Während ich auf eine Antwort von Service1 "warte", kann ich Service2, Service3 usw. anrufen.

Zum Beispiel siehe unten Code:

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

// Use service1Response and service2Response

Nun service2Responseist nicht abhängig vonservice1Response und sie können unabhängig abgerufen werden. Daher muss ich nicht auf die Antwort des ersten Dienstes warten, um den zweiten Dienst anzurufen.

Ich glaube nicht, dass ich es gebrauchen kann Parallel.ForEach , da es sich nicht um eine CPU-gebundene Operation handelt.

Kann ich use aufrufen, um diese beiden Operationen gleichzeitig aufzurufen Task.WhenAll? Ein Problem bei der Verwendung Task.WhenAllist, dass keine Ergebnisse zurückgegeben werden. Um das Ergebnis abzurufen, kann ich task.Resultnach dem Aufruf anrufen Task.WhenAll, da alle Aufgaben bereits erledigt sind und ich uns nur noch eine Antwort holen muss?

Beispielcode:

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

await Task.WhenAll(task1, task2)

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

// Use result1 and result2

Ist dieser Code in Bezug auf die Leistung besser als der erste? Irgendein anderer Ansatz, den ich verwenden kann?

Ankit Vijay
quelle
I do not think I can use Parallel.ForEach here since it is not CPU bound operation- Ich sehe die Logik dort nicht. Parallelität ist Parallelität.
Robert Harvey
3
@RobertHarvey Ich vermute, die Sorge ist, dass in diesem Zusammenhang Parallel.ForEachneue Threads erzeugt werden, während async awaitalles in einem einzigen Thread ausgeführt wird.
MetaFight
@Ankit es hängt davon ab, wann der Code blockiert werden soll. Ihr zweites Beispiel würde blockieren, bis beide Antworten fertig sind. Ihr erstes Beispiel würde vermutlich nur dann logisch blockieren, wenn der Code versuchen würde, die response ( await) zu verwenden, bevor sie fertig ist.
MetaFight
Es könnte einfacher sein, eine zufriedenstellendere Antwort zu geben, wenn Sie ein weniger abstraktes Beispiel für den Code angegeben haben, der beide Dienstantworten belegt.
MetaFight
@MetaFight In meinem zweiten Beispiel gehe WhenAllich davon aus, Resultdass alle Aufgaben abgeschlossen sind, bevor .Result aufgerufen wird. Da Task.Result den aufrufenden Thread blockiert, gehe ich davon aus, dass wenn ich ihn nach Abschluss der Tasks aufrufe, er das Ergebnis sofort zurückgibt. Ich möchte das Verständnis bestätigen.
Ankit Vijay

Antworten:

17

Ein Problem bei der Verwendung von Task.WhenAll ist, dass keine Ergebnisse zurückgegeben werden

Aber es gibt die Ergebnisse zurück. Sie befinden sich alle in einem Array eines gemeinsamen Typs. Daher ist es nicht immer nützlich , die Ergebnisse dahingehend zu verwenden, dass Sie das Element im Array suchen müssen, das Taskdem gewünschten Ergebnis entspricht, und es möglicherweise in das zugehörige Element umwandeln Tatsächlicher Typ, daher ist dies in diesem Zusammenhang möglicherweise nicht der einfachste / am besten lesbare Ansatz. Wenn Sie jedoch alle Ergebnisse aus jeder Aufgabe haben möchten und der übliche Typ der Typ ist, als den Sie sie behandeln möchten, ist dies großartig .

Zum Abrufen des Ergebnisses kann ich task.Result aufrufen, nachdem Task.WhenAll aufgerufen wurde, da alle Aufgaben bereits abgeschlossen sind und ich nur die Antwort abrufen muss?

Ja, das könntest du machen. Sie könnten auch awaitsie ( awaitwürde die Ausnahme in einer fehlerhaften Aufgabe auspacken, während Resultwürde eine aggregierte Ausnahme auslösen , aber sonst wäre es das gleiche).

Ist dieser Code in Bezug auf die Leistung besser als der erste?

Es führt die beiden Operationen zur gleichen Zeit aus und nicht die eine und die andere. Ob dies besser oder schlechter ist, hängt von den zugrunde liegenden Vorgängen ab. Wenn die zugrunde liegenden Vorgänge "eine Datei von der Festplatte lesen" sind, ist es wahrscheinlich langsamer, sie parallel auszuführen, da es nur einen Plattenkopf gibt und er sich zu einem bestimmten Zeitpunkt nur an einem Ort befinden kann. Das Herumspringen zwischen zwei Dateien ist langsamer als das Lesen einer Datei als der anderen. Wenn die Vorgänge dagegen "Netzwerkanfragen ausführen" (wie hier der Fall), sind sie sehr wahrscheinlich schneller (zumindest bis zu einer bestimmten Anzahl gleichzeitiger Anfragen), da Sie auf eine Antwort warten können Von einem anderen Netzwerkcomputer genauso schnell, wenn auch eine andere ausstehende Netzwerkanforderung aktiv ist. Wenn Sie wissen wollen, ob es

Irgendein anderer Ansatz, den ich verwenden kann?

Wenn es für Sie nicht wichtig ist, dass Sie alle Ausnahmen kennen, die für alle Vorgänge gelten, die Sie parallel ausführen, und nicht nur für den ersten, können Sie awaitdie Aufgaben WhenAllganz einfach ausführen . Das Einzige, WhenAllwas Sie bekommen, ist, AggregateExceptionmit jeder einzelnen Ausnahme von jeder fehlerhaften Aufgabe eine zu haben, anstatt zu werfen, wenn Sie die erste fehlerhafte Aufgabe treffen. Es ist so einfach wie:

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

var result1 = await task1;
var result2 = await task2;
Servy
quelle
Das heißt nicht, dass gleichzeitig Aufgaben ausgeführt werden, geschweige denn parallel. Sie warten darauf, dass die einzelnen Aufgaben nacheinander ausgeführt werden. Völlig in Ordnung, wenn Sie sich nicht für performanten Code interessieren.
Rick O'Shea
3
@ RickO'Shea Es beginnt die Operationen der Reihe nach . Der zweite Vorgang wird gestartet, nachdem * der erste Vorgang gestartet wurde . Der Start des asynchronen Vorgangs sollte jedoch im Grunde genommen sofort erfolgen (wenn dies nicht der Fall ist, ist er nicht wirklich asynchron, und das ist ein Fehler in dieser Methode). Nach dem Starten des einen und des anderen wird es erst fortgesetzt, nachdem der erste und der zweite Zieleinlauf beendet wurden. Da nichts darauf wartet, dass das erste beendet wird, bevor das zweite gestartet wird, verhindert nichts, dass sie gleichzeitig ausgeführt werden (was dasselbe ist, als wenn sie parallel ausgeführt werden).
Servy
@Servy Ich glaube nicht, dass das stimmt. Ich habe die Protokollierung innerhalb von zwei asynchronen Vorgängen hinzugefügt, die jeweils ungefähr eine Sekunde dauerten (beide führen http-Aufrufe durch), und sie dann aufgerufen, wie Sie vorgeschlagen haben, und sicher genug, dass Task1 gestartet und beendet wurde und Task2 gestartet und beendet wurde.
Matt Frear
@MattFrear Dann war die Methode tatsächlich nicht asynchron. Es war synchron. Per Definition kehrt eine asynchrone Methode sofort zurück, anstatt nach Abschluss der Operation zurückzukehren.
Servy
@Servy per definitionem bedeutet das Warten, dass Sie warten, bis die asynchrone Task beendet ist, bevor Sie die nächste Zeile ausführen. Ist es nicht
Matt Frear
0

Hier ist die Erweiterungsmethode, bei der SemaphoreSlim verwendet wird und ein Höchstmaß an Parallelität eingestellt werden kann

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

Beispielnutzung:

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

Sie können entweder verwenden

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

oder

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
quelle
Warum Abstimmungen?
user1451111