Warum sollten Sie jemals auf eine Methode 'warten' und dann sofort ihren Rückgabewert abfragen?

24

In diesem MSDN-Artikel wird der folgende Beispielcode bereitgestellt (der Kürze halber leicht bearbeitet):

public async Task<ActionResult> Details(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }

    Department department = await db.Departments.FindAsync(id);

    if (department == null)
    {
        return HttpNotFound();
    }

    return View(department);
}

Die FindAsyncMethode ruft ein DepartmentObjekt anhand seiner ID ab und gibt a zurück Task<Department>. Dann wird die Abteilung sofort überprüft, um festzustellen, ob sie null ist. Soweit ich weiß, wird die Abfrage des Task-Werts auf diese Weise die Codeausführung blockieren, bis der Wert der erwarteten Methode zurückgegeben wird, wodurch dies effektiv zu einem synchronen Aufruf wird.

Warum würdest du das jemals tun? Wäre es nicht einfacher, nur die synchrone Methode aufzurufen Find(id), wenn Sie trotzdem sofort blockieren würden?

Robert Harvey
quelle
Es könnte umsetzungsbezogen sein. ... else return null;Dann müssten Sie überprüfen, ob die Methode tatsächlich die von Ihnen gewünschte Abteilung gefunden hat.
Jeremy Kato
Ich sehe keine in einem asp.net, aber in einer Destop-App, wenn du es auf diese Weise tust, frierst du die Benutzeroberfläche nicht ein
Rémi
Hier ist ein Link, der das Warten-
Jon Raynor
Das Warten lohnt sich in ASP.NET nur, wenn Sie durch Thread-Kontaktschalter langsamer werden oder die Speichernutzung durch viele Threads ein Problem darstellt.
Ian

Antworten:

24

So wie ich es verstehe, blockiert die Abfrage des Task-Werts auf diese Weise die Codeausführung, bis der Wert der erwarteten Methode zurückgegeben wird, wodurch dies effektiv zu einem synchronen Aufruf wird.

Nicht ganz.

Beim Aufrufen wird await db.Departments.FindAsync(id)die Aufgabe abgesetzt und der aktuelle Thread zur Verwendung durch andere Vorgänge an den Pool zurückgegeben. Der Ablauf der Ausführung ist blockiert ( departmentwenn ich die Dinge richtig verstehe, ist dies unabhängig von der Verwendung direkt danach), aber der Thread selbst kann von anderen Dingen verwendet werden, während Sie warten, bis der Vorgang außerhalb des Computers abgeschlossen ist (und durch einen Ereignis- oder Abschlussport signalisiert).

Wenn Sie angerufen haben, d.Departments.Find(id)sitzt der Thread dort und wartet auf die Antwort, obwohl der größte Teil der Verarbeitung in der Datenbank ausgeführt wird.

Sie setzen CPU-Ressourcen effektiv frei, wenn die Festplatte gebunden ist.

Telastyn
quelle
2
Aber ich dachte, alles, was awaitgetan wurde, war, den Rest der Methode als Fortsetzung im selben Thread zu signieren (es gibt Ausnahmen; einige asynchrone Methoden drehen ihren eigenen Thread hoch) oder die asyncMethode als Fortsetzung im selben Thread zu signieren und zuzulassen Der verbleibende Code muss ausgeführt werden (wie Sie sehen können, ist mir nicht ganz klar, wie das asyncfunktioniert). Was Sie beschreiben, klingt eher wie eine raffinierte Form vonThread.Sleep(untilReturnValueAvailable)
Robert Harvey
1
@RobertHarvey - es weist es als Fortsetzung zu, aber sobald Sie die Aufgabe (und ihre Fortsetzung) zur Verarbeitung gesendet haben, ist nichts mehr zu tun. Es ist nicht garantiert, dass es sich auf demselben Thread befindet, es sei denn, Sie geben es an (über ConfigureAwaitiirc).
Telastyn
1
Siehe meine Antwort ... die Fortsetzung wird standardmäßig auf den ursprünglichen Thread zurückgesetzt.
Michael Brown
2
Ich glaube ich sehe was ich hier vermisse. Damit dies einen Vorteil bietet, muss das ASP.NET MVC-Framework awaitseinen Aufruf an public async Task<ActionResult> Details(int? id). Andernfalls wird der ursprüngliche Anruf nur blockiert und wartet auf department == nulldie Lösung.
Robert Harvey
2
@RobertHarvey Zum Zeitpunkt der await ...Rückkehr ist der FindAsyncAnruf beendet. Das ist, was das Warten macht. Es heißt warten, weil es Ihren Code auf Dinge warten lässt. (Beachten Sie jedoch, dass dies nicht mit dem Warten des aktuellen Threads identisch ist.)
user253751
17

Ich hasse es wirklich, dass keines der Beispiele zeigt, wie es möglich ist, ein paar Zeilen zu warten, bevor man auf die Aufgabe wartet. Bedenken Sie.

Foo foo = await getFoo();
Bar bar = await getBar();

Console.WriteLine(“Do some other stuff to prepare.”);

doStuff(foo, bar);

Dies ist die Art von Code, die die Beispiele anregen, und Sie haben Recht. Das hat wenig Sinn. Es gibt den Haupt-Thread frei, um andere Dinge zu tun, wie z. B. auf Eingaben über die Benutzeroberfläche zu reagieren, aber die wahre Stärke von async / await ist, dass ich problemlos andere Dinge tun kann, während ich darauf warte, dass eine möglicherweise lange laufende Aufgabe abgeschlossen ist. Der obige Code wird "blockieren" und warten, bis die Druckzeile ausgeführt ist, bis wir Foo & Bar erhalten haben. Es besteht kein Grund zu warten. Wir können das verarbeiten, während wir warten.

Task<Foo> foo = getFoo();
Task<Bar> bar = getBar();

Console.WriteLine(“Do some other stuff to prepare.”);

doStuff(await foo, await bar);

Mit dem neu geschriebenen Code hören wir jetzt nicht auf und warten auf unsere Werte, bis wir müssen. Ich bin immer auf der Suche nach solchen Möglichkeiten. Wenn Sie überlegen sind, wann Sie auf uns warten, kann dies zu erheblichen Leistungsverbesserungen führen. Wir haben heutzutage mehrere Kerne, können sie aber auch verwenden.

Badeente
quelle
1
Hm, es geht weniger um Kerne / Threads als darum, asynchrone Aufrufe richtig zu verwenden.
Deduplikator
Du liegst nicht falsch @Deduplicator. Deshalb habe ich in meiner Antwort "block" angegeben. Es ist schwer, mit diesem Material zu sprechen, ohne die Threads zu erwähnen, aber obwohl man immer noch anerkennt, dass es sich möglicherweise um Multithreading handelt oder nicht.
RubberDuck
Ich schlage vor, Sie seien vorsichtig, wenn Sie in einem anderen Methodenaufruf warten. Manchmal versteht der Compiler das Warten falsch und Sie bekommen eine wirklich seltsame Ausnahme. Nachdem async / await nur Syntaxzucker ist, können Sie mit sharplab.io überprüfen, wie der generierte Code aussieht. Ich habe das mehrmals beobachtet und jetzt warte ich nur auf eine Zeile über dem Anruf, wo das Ergebnis benötigt wird ... brauche diese Kopfschmerzen nicht.
Evictednoise
"Das hat wenig Sinn." - Das hat viel Sinn. Die Fälle, in denen "weiterhin andere Dinge tun" auf die gleiche Weise anwendbar ist, sind nicht so häufig. Es ist weitaus üblicher, dass Sie nur wollen awaitund den Thread stattdessen ganz andere Dinge tun lassen.
Sebastian Redl
Als ich sagte, "das macht wenig Sinn" @SebastianRedl, meinte ich den Fall, in dem Sie offensichtlich beide Aufgaben parallel ausführen könnten, anstatt zu laufen, zu warten, zu laufen, zu warten. Dies ist weitaus häufiger, als Sie zu glauben scheinen. Ich wette, wenn Sie sich in Ihrer Codebasis umsehen, finden Sie Möglichkeiten.
RubberDuck
6

Hinter den Kulissen passiert also noch mehr. Async / Await ist syntaktischer Zucker. Schauen Sie sich zunächst die Signatur der FindAsync-Funktion an. Es gibt eine Aufgabe zurück. Schon sehen Sie die Magie des Schlüsselworts, es entpackt diese Aufgabe in eine Abteilung.

Die aufrufende Funktion blockiert nicht. Was passiert, ist, dass die Zuweisung zu Abteilung und alles, was auf das Schlüsselwort await folgt, in einen Abschluss eingeschlossen und für alle Absichten und Zwecke an die Task.ContinueWith-Methode übergeben wird (die FindAsync-Funktion wird automatisch in einem anderen Thread ausgeführt).

Natürlich passiert hinter den Kulissen noch mehr, da die Operation auf den ursprünglichen Thread zurückgeführt wird (sodass Sie sich nicht mehr um die Synchronisierung mit der Benutzeroberfläche kümmern müssen, wenn Sie eine Hintergrundoperation ausführen) und wenn die aufrufende Funktion asynchron ist ( und asynchron aufgerufen wird, passiert das Gleiche auf dem Stack.

Was also passiert, ist, dass Sie die Magie von Async-Operationen ohne die Fallen erhalten.

Michael Brown
quelle
1

Nein, es kehrt nicht sofort zurück. Durch das Warten wird der Methodenaufruf asynchron. Wenn FindAsync aufgerufen wird, gibt die Details-Methode eine nicht abgeschlossene Aufgabe zurück. Wenn FindAsync beendet ist, wird das Ergebnis in die Abteilungsvariable zurückgegeben und der Rest der Details-Methode fortgesetzt.

Steve
quelle
Nein, es gibt überhaupt keine Blockierung. Erst wenn die asynchrone Methode bereits abgeschlossen ist, wird department == null erreicht.
Steve
1
async awaitIm Allgemeinen werden keine neuen Threads erstellt, und selbst wenn dies der Fall ist, müssen Sie immer noch auf diesen Abteilungswert warten, um herauszufinden, ob er null ist.
Robert Harvey
2
Sie sagen also im Grunde, dass, damit dies überhaupt von Nutzen ist, der Aufruf zu public async Task<ActionResult> auch awaitbearbeitet werden muss.
Robert Harvey
2
@RobertHarvey Ja, du hast die Idee. Async / Await ist im Wesentlichen viral. Wenn Sie eine Funktion "abwarten", sollten Sie auch alle Funktionen abwarten, die sie aufrufen. awaitDarf nicht mit .Wait()oder gemischt werden .Result, da dies zu Deadlocks führen kann. Die asynchrone / erwartete Kette endet normalerweise bei einer Funktion mit einer async voidSignatur, die hauptsächlich für Ereignishandler oder für Funktionen verwendet wird, die direkt von UI-Elementen aufgerufen werden.
KChaloux
1
@Steve - " ... so wäre es auf einem eigenen Thread. " Nein, gelesene Tasks sind immer noch keine Threads und async ist nicht parallel . Dies schließt die tatsächliche Ausgabe ein, die zeigt, dass kein neuer Thread erstellt wurde.
ToolmakerSteve
1

Ich stelle mir "asynchron" gerne wie einen Vertrag vor, einen Vertrag, der besagt, dass "ich dies bei Bedarf asynchron ausführen kann, aber Sie können mich auch wie jede andere Synchronfunktion aufrufen".

Das heißt, ein Entwickler hat Funktionen erstellt, und einige Designentscheidungen haben dazu geführt, dass einige Funktionen als "asynchron" gekennzeichnet wurden. Dem Aufrufer / Verbraucher der Funktionen steht es frei, diese nach eigenem Ermessen zu verwenden. Wie Sie sagen, können Sie entweder unmittelbar vor dem Funktionsaufruf warten und darauf warten. Auf diese Weise haben Sie die Funktion wie eine synchrone Funktion behandelt. Wenn Sie jedoch möchten, können Sie sie ohne Wartezeit als aufrufen

Task<Department> deptTask = db.Departments.FindAsync(id);

und nach etwa 10 Zeilen nach unten die Funktion, die Sie aufrufen

Department d = await deptTask;

daher wird es als asynchrone Funktion behandelt.

Es liegt an dir.

user734028
quelle
1
Es ist eher so: "Ich behalte mir das Recht vor, die Nachricht asynchron zu verarbeiten, und verwende dies, um das Ergebnis zu erhalten."
Deduplikator
-1

"wenn du sowieso sofort blocken willst" ist die Antwort "Ja". Nur wenn Sie eine schnelle Antwort benötigen, ist wait / async sinnvoll. Beispielsweise wird der UI-Thread zu einer wirklich asynchronen Methode, der UI-Thread kehrt zurück und hört weiterhin auf das Klicken auf die Schaltfläche, während der Code unter "wait" von einem anderen Thread ausgeführt wird und schließlich das Ergebnis abruft.

user10200952
quelle
1
Was bietet diese Antwort, was die anderen Antworten nicht bieten?