Synchronen Code in asynchronen Aufruf einbinden

93

Ich habe eine Methode in der ASP.NET-Anwendung, deren Fertigstellung ziemlich viel Zeit in Anspruch nimmt. Ein Aufruf dieser Methode kann je nach Cache-Status und vom Benutzer angegebenen Parametern bis zu dreimal während einer Benutzeranforderung erfolgen. Jeder Anruf dauert ca. 1-2 Sekunden. Die Methode selbst ist ein synchroner Aufruf des Dienstes und es gibt keine Möglichkeit, die Implementierung zu überschreiben.
Der synchrone Aufruf des Dienstes sieht also ungefähr so ​​aus:

public OutputModel Calculate(InputModel input)
{
    // do some stuff
    return Service.LongRunningCall(input);
}

Und die Verwendung der Methode ist (beachten Sie, dass der Aufruf der Methode mehr als einmal vorkommen kann):

private void MakeRequest()
{
    // a lot of other stuff: preparing requests, sending/processing other requests, etc.
    var myOutput = Calculate(myInput);
    // stuff again
}

Ich habe versucht, die Implementierung von meiner Seite zu ändern, um die gleichzeitige Arbeit mit dieser Methode zu ermöglichen, und hier ist, worauf ich bisher gekommen bin.

public async Task<OutputModel> CalculateAsync(InputModel input)
{
    return await Task.Run(() =>
    {
        return Calculate(input);
    });
}

Verwendung (ein Teil des Codes "Andere Dinge erledigen" wird gleichzeitig mit dem Aufruf des Dienstes ausgeführt):

private async Task MakeRequest()
{
    // do some stuff
    var task = CalculateAsync(myInput);
    // do other stuff
    var myOutput = await task;
    // some more stuff
}

Meine Frage lautet wie folgt. Benutze ich den richtigen Ansatz, um die Ausführung in der ASP.NET-Anwendung zu beschleunigen, oder mache ich unnötige Arbeit, um synchronen Code asynchron auszuführen? Kann jemand erklären, warum der zweite Ansatz in ASP.NET keine Option ist (wenn dies wirklich nicht der Fall ist)? Wenn ein solcher Ansatz anwendbar ist, muss ich eine solche Methode asynchron aufrufen, wenn dies der einzige Aufruf ist, den wir derzeit ausführen können (ich habe einen solchen Fall, in dem keine anderen Aufgaben zu erledigen sind, während auf den Abschluss gewartet wird)?
Die meisten Artikel im Internet zu diesem Thema behandeln die Verwendung des async-awaitAnsatzes mit dem Code, der bereits awaitableMethoden bereitstellt , aber das ist nicht mein Fall. Hierist der schöne Artikel, der meinen Fall beschreibt, der die Situation von parallelen Anrufen nicht beschreibt und die Option zum Umbrechen von Synchronisierungsanrufen ablehnt, aber meiner Meinung nach ist meine Situation genau die Gelegenheit, dies zu tun.
Vielen Dank im Voraus für Hilfe und Tipps.

Eadel
quelle

Antworten:

114

Es ist wichtig, zwischen zwei verschiedenen Arten der Parallelität zu unterscheiden. Asynchrone Parallelität ist, wenn Sie mehrere asynchrone Vorgänge im Flug haben (und da jeder Vorgang asynchron ist, verwendet keiner von ihnen tatsächlich einen Thread ). Parallele Parallelität liegt vor, wenn mehrere Threads jeweils einen separaten Vorgang ausführen.

Als erstes müssen Sie diese Annahme neu bewerten:

Die Methode selbst ist ein synchroner Aufruf des Dienstes und es gibt keine Möglichkeit, die Implementierung zu überschreiben.

Wenn Ihr "Dienst" ein Webdienst oder etwas anderes ist, das an E / A gebunden ist, besteht die beste Lösung darin, eine asynchrone API dafür zu schreiben.

Ich gehe davon aus, dass Ihr "Dienst" eine CPU-gebundene Operation ist, die auf demselben Computer wie der Webserver ausgeführt werden muss.

Wenn dies der Fall ist, ist als nächstes eine andere Annahme zu bewerten:

Ich brauche die Anfrage, um schneller ausgeführt zu werden.

Sind Sie absolut sicher, dass Sie das tun müssen? Gibt es Front-End-Änderungen, die Sie stattdessen vornehmen können - z. B. die Anforderung starten und dem Benutzer erlauben, während der Verarbeitung andere Arbeiten auszuführen?

Ich gehe davon aus, dass Sie die individuelle Anforderung wirklich schneller ausführen müssen.

In diesem Fall müssen Sie parallelen Code auf Ihrem Webserver ausführen. Dies wird im Allgemeinen definitiv nicht empfohlen, da der parallele Code Threads verwendet, die ASP.NET möglicherweise zur Verarbeitung anderer Anforderungen benötigt, und durch Entfernen / Hinzufügen von Threads die Heuristiken des ASP.NET-Threadpools deaktiviert werden. Diese Entscheidung wirkt sich also auf Ihren gesamten Server aus.

Wenn Sie in ASP.NET parallelen Code verwenden, treffen Sie die Entscheidung, die Skalierbarkeit Ihrer Webanwendung wirklich einzuschränken. Möglicherweise sehen Sie auch eine beträchtliche Menge an Thread-Abwanderung, insbesondere wenn Ihre Anforderungen überhaupt platzen. Ich empfehle, unter ASP.NET nur parallelen Code zu verwenden, wenn Sie wissen, dass die Anzahl der gleichzeitigen Benutzer sehr gering ist (dh kein öffentlicher Server).

Wenn Sie also so weit kommen und sicher sind, dass Sie eine parallele Verarbeitung unter ASP.NET durchführen möchten, haben Sie mehrere Möglichkeiten.

Eine der einfacheren Methoden ist die Verwendung Task.Run, die Ihrem vorhandenen Code sehr ähnlich ist. Ich empfehle jedoch nicht, eine CalculateAsyncMethode zu implementieren , da dies impliziert, dass die Verarbeitung asynchron ist (was nicht der Fall ist). Verwenden Sie stattdessen Task.Runam Punkt des Anrufs:

private async Task MakeRequest()
{
  // do some stuff
  var task = Task.Run(() => Calculate(myInput));
  // do other stuff
  var myOutput = await task;
  // some more stuff
}

Alternativ kann , wenn es gut mit Ihrem Code funktioniert, können Sie die Verwendung ParallelArt, das heißt Parallel.For, Parallel.ForEachoder Parallel.Invoke. Der Vorteil des ParallelCodes besteht darin, dass der Anforderungsthread als einer der parallelen Threads verwendet wird und dann die Ausführung im Thread-Kontext fortsetzt (es gibt weniger Kontextwechsel als im asyncBeispiel):

private void MakeRequest()
{
  Parallel.Invoke(() => Calculate(myInput1),
      () => Calculate(myInput2),
      () => Calculate(myInput3));
}

Ich empfehle die Verwendung von Parallel LINQ (PLINQ) unter ASP.NET überhaupt nicht.

Stephen Cleary
quelle
1
Vielen Dank für eine ausführliche Antwort. Noch etwas möchte ich klarstellen. Wenn ich mit der Ausführung beginne Task.Run, wird der Inhalt in einem anderen Thread ausgeführt, der aus dem Thread-Pool stammt. Dann besteht überhaupt keine Notwendigkeit, blockierende Aufrufe (wie z. B. meinen Aufruf zum Dienst) in Runs zu verpacken , da sie immer jeweils einen Thread verbrauchen, der während der Ausführung der Methode blockiert wird. In einer solchen Situation besteht der einzige Vorteil, der async-awaitin meinem Fall übrig bleibt, darin, mehrere Aktionen gleichzeitig auszuführen. Bitte korrigiere mich wenn ich falsch liege.
Eadel
2
Ja, der einzige Vorteil, den Sie von Run(oder Parallel) erhalten, ist die Parallelität. Die Operationen blockieren immer noch jeweils einen Thread. Da Sie sagen, dass der Dienst ein Webdienst ist, empfehle ich nicht, Runoder zu verwenden Parallel. Schreiben Sie stattdessen eine asynchrone API für den Dienst.
Stephen Cleary
3
@ StephenCleary. Was meinst du mit "... und da jede Operation asynchron ist, verwendet keiner von ihnen tatsächlich einen Thread ..." danke!
Alltej
3
@alltej: Für wirklich asynchronen Code gibt es keinen Thread .
Stephen Cleary
4
@ JoshuaFrank: Nicht direkt. Code hinter einer synchronen API muss per Definition den aufrufenden Thread blockieren. Sie können eine asynchrone API wie diese verwenden BeginGetResponseund mehrere davon starten und dann (synchron) blockieren, bis alle vollständig sind. Diese Vorgehensweise ist jedoch schwierig - siehe blog.stephencleary.com/2012/07/dont-block-on -async-code.html und msdn.microsoft.com/en-us/magazine/mt238404.aspx . asyncWenn möglich, ist es normalerweise einfacher und sauberer, den gesamten Weg zu übernehmen .
Stephen Cleary
1

Ich habe festgestellt, dass der folgende Code eine Aufgabe so konvertieren kann, dass sie immer asynchron ausgeführt wird

private static async Task<T> ForceAsync<T>(Func<Task<T>> func)
{
    await Task.Yield();
    return await func();
}

und ich habe es auf folgende Weise verwendet

await ForceAsync(() => AsyncTaskWithNoAwaits())

Dadurch wird jede Aufgabe asynchron ausgeführt, sodass Sie sie in WhenAll-, WhenAny-Szenarien und anderen Verwendungszwecken kombinieren können.

Sie können auch einfach Task.Yield () als erste Zeile Ihres aufgerufenen Codes hinzufügen.

Kernowcode
quelle