wake-up-neo.com

Wann ist der beste Ort, um Task.Result zu verwenden, anstatt auf Task zu warten

Während ich in .NET seit einiger Zeit asynchronen Code verwende, habe ich erst kürzlich damit begonnen, ihn zu recherchieren und zu verstehen, was los ist. Ich habe gerade meinen Code durchgearbeitet und versucht, ihn zu ändern. Wenn eine Aufgabe parallel zu einer bestimmten Arbeit ausgeführt werden kann, ist dies der Fall. Also zum Beispiel:

var user = await _userRepo.GetByUsername(User.Identity.Name);

//Some minor work that doesn't rely on the user object

user = await _userRepo.UpdateLastAccessed(user, DateTime.Now);

return user;

Jetzt wird:

var userTask = _userRepo.GetByUsername(User.Identity.Name);

//Some work that doesn't rely on the user object

user = await _userRepo.UpdateLastAccessed(userTask.Result, DateTime.Now);

return user;

Ich verstehe, dass das Benutzerobjekt jetzt aus der Datenbank abgerufen wird, WÄHREND einige nicht verwandte Arbeiten ausgeführt werden. Dinge, die ich gesehen habe, deuten jedoch darauf hin, dass das Ergebnis selten verwendet werden sollte und das Abwarten bevorzugt wird, aber ich verstehe nicht, warum ich warten möchte, bis mein Benutzerobjekt abgerufen wird, wenn ich eine andere unabhängige Logik am ausführen kann gleiche Zeit?

6
George Harnwell

Stellen wir sicher, dass die Lede hier nicht vergraben wird:

Also zum Beispiel: [irgendein richtiger Code] wird [irgendein falscher Code]

TUN SIE NIEMALS NIEMALS DAS.

Ihr Instinkt, den Kontrollfluss neu zu strukturieren, um die Leistung zu verbessern, ist hervorragend und korrekt. Die Verwendung von Result ist FALSCH FALSCH FALSCH.

Die richtige Art, Ihren Code umzuschreiben, ist

var userTask = _userRepo.GetByUsername(User.Identity.Name);    
//Some work that doesn't rely on the user object    
user = await _userRepo.UpdateLastAccessed(await userTask, DateTime.Now);    
return user;

Denken Sie daran, Warten macht einen Anruf nicht asynchron. Warten bedeutet einfach: "Wenn das Ergebnis dieser Aufgabe noch nicht verfügbar ist, machen Sie etwas anderes und kehren Sie hierher zurück, wenn es verfügbar ist." Der Aufruf ist bereits asynchron: gibt eine Aufgabe zurück.

Die Leute scheinen zu glauben, dass await die Semantik eines Co-Calls hat; Es tut nicht. Erwarten Sie vielmehr die - Extraktionsoperation für die Task comonad ; Es ist ein Operator für Tasks , nicht für Aufrufausdrücke . Normalerweise wird es bei Methodenaufrufen nur angezeigt, weil es ein gängiges Muster ist, eine asynchrone Operation als Methode zu abstrahieren. Die zurückgegebene Aufgabe ist das, worauf gewartet wird, nicht der Aufruf.

Dinge, die ich gesehen habe, deuten jedoch darauf hin, dass das Ergebnis selten verwendet werden sollte und das Abwarten bevorzugt wird, aber ich verstehe nicht, warum ich warten möchte, bis mein Benutzerobjekt abgerufen wird, wenn ich eine andere unabhängige Logik am ausführen kann gleiche Zeit?

Warum glauben Sie, dass Sie mit Result gleichzeitig eine andere unabhängige Logik ausführen können? Ergebnis verhindert, dass Sie genau das tun. Ergebnis ist ein synchrones Warten . Ihr Thread kann keine andere Arbeit ausführen, während er synchron auf den Abschluss der Aufgabe wartet. Verwenden Sie ein asynchrones Warten , um die Effizienz zu verbessern. Denken Sie daran, dass await einfach bedeutet, dass dieser Workflow erst dann fortgesetzt werden kann, wenn diese Aufgabe abgeschlossen ist. Wenn er also nicht abgeschlossen ist, suchen Sie nach mehr zu erledigender Arbeit und kehren Sie später zurück. Ein zu frühes await kann, wie Sie bemerken, zu einem ineffizienten Workflow führen, da der Workflow manchmal fortschreiten kann, auch wenn die Aufgabe nicht abgeschlossen ist.

Auf jeden Fall bewegen Sie sich dorthin, wo das Warten stattfindet, um die Effizienz Ihres Workflows zu verbessern, aber ändern Sie sie niemals nie in Result. Sie haben ein tiefes Missverständnis darüber, wie asynchrone Workflows funktionieren, wenn Sie glauben, dass die Verwendung von Result jemals die Effizienz der Parallelität im Workflow verbessern wird . Untersuche deinen Glauben und finde heraus, welcher dir diese falsche Intuition gibt.

Der Grund, warum Sie Result niemals so verwenden dürfen, ist nicht nur, dass es ineffizient ist, synchron zu warten, wenn ein asynchroner Workflow ausgeführt wird. Es wird irgendwann Ihren Prozess hängen. Betrachten Sie den folgenden Workflow:

  • task1 repräsentiert einen Job, dessen Ausführung für diesen Thread in Zukunft geplant ist und ein Ergebnis liefert.
  • asynchrone Funktion Foo wartet auf Task1.
  • task1 ist noch nicht abgeschlossen, daher kehrt Foo zurück, sodass dieser Thread mehr Arbeit ausführen kann. Foo gibt eine Aufgabe zurück, die seinen Arbeitsablauf darstellt, und registriert das Abschließen dieser Aufgabe als das Abschließen von task1.
  • Der Thread kann jetzt in Zukunft frei arbeiten, einschließlich task1.
  • task1 wird abgeschlossen, wodurch die Ausführung des Workflows von Foo und schließlich die den Workflow von Foo darstellende Aufgabe ausgelöst wird.

Nehmen wir nun an, Foo holt stattdessen Result von task1. Was geschieht? Foo wartet synchron auf task1 to complete, das darauf wartet, dass der aktuelle Thread verfügbar wird, was niemals passiert, weil synchron gewartet wird . Das Aufrufen von Result bewirkt, dass ein Thread mit sich selbst blockiert , wenn die Task in irgendeiner Weise mit dem aktuellen Thread verknüpft ist.. Sie können jetzt Deadlocks ohne Sperren und mit nur einem Thread erstellen! Mach das nicht.

21
Eric Lippert

In Ihrem Fall können Sie verwenden:

user = await _userRepo.UpdateLastAccessed(await userTask, DateTime.Now);

oder vielleicht klarer:

var user = await _userRepo.GetByUsername(User.Identity.Name);
//Some work that doesn't rely on the user object
user = await _userRepo.UpdateLastAccessed(user, DateTime.Now);

Das einzige Mal, das Sie .Result berühren sollten, ist, wenn Sie wissen, dass die Aufgabe abgeschlossen wurde. Dies kann in einigen Szenarien nützlich sein, in denen Sie versuchen, das Erstellen eines Zustandsautomaten für async zu vermeiden, und Sie glauben, dass die Aufgabe mit hoher Wahrscheinlichkeit synchron abgeschlossen wurde (möglicherweise mithilfe einer lokalen Funktion für den Fall async) oder wenn Sie Rückrufe verwenden statt async/await und du bist im Rückruf .

Als Beispiel zur Vermeidung einer Zustandsmaschine:

ValueTask<int> FetchAndProcess(SomeArgs args) {
    async ValueTask<int> Awaited(ValueTask<int> task) => SomeOtherProcessing(await task);
    var task = GetAsyncData(args);
    if (!task.IsCompletedSuccessfully) return Awaited(task);
    return new ValueTask<int>(SomeOtherProcessing(task.Result));
}

Der Punkt hier ist, dass wenn GetAsyncData ein synchron vervollständigtes Ergebnis zurückgibt, wir alle async-Maschinen vollständig meiden.

4
Marc Gravell

Async await bedeutet nicht, dass Ihr Code in mehreren Threads ausgeführt wird.

Dies verringert jedoch die Zeit, in der der Thread untätig auf den Abschluss von Prozessen wartet und somit früher beendet wird.

Wenn der Thread normalerweise untätig warten muss, bis etwas beendet ist, z. B. warten, bis eine Webseite heruntergeladen wurde, eine Datenbankabfrage beendet wurde, ein Festplattenschreibvorgang beendet wurde, wartet der asynchrone Thread nicht untätig, bis die Daten geschrieben wurden/abgerufen, schaut sich aber um, ob es stattdessen andere Dinge tun kann, und kommt später zurück, wenn die erwartete Aufgabe abgeschlossen ist.

Dies wurde mit einer Kochanalogie in diesem Interview mit Eric Lippert beschrieben. Suchen Sie irgendwo in der Mitte nach asynchronem Warten.

Eric Lippert vergleicht das asynchrone Warten mit einem (!) Koch, der frühstücken muss. Nachdem er angefangen hat, das Brot zu rösten, kann er untätig warten, bis das Brot geröstet ist, bevor er den Wasserkocher auf Tee setzt, bis das Wasser kocht, bevor er die Teeblätter in die Teekanne legt usw.

Ein asynchrone Köchin würde nicht auf das geröstete Brot warten, sondern den Wasserkocher aufsetzen, und während sich das Wasser erwärmt, würde er die Teeblätter in die Teekanne geben.

Wann immer der Koch untätig auf etwas warten muss, schaut er sich um, um zu sehen, ob er stattdessen etwas anderes tun kann.

Ein Thread in einer asynchronen Funktion führt etwas Ähnliches aus. Da die Funktion asynchron ist, wissen Sie, dass die Funktion irgendwo warten muss. In der Tat, wenn Sie vergessen, das Warten zu programmieren, wird Ihr Compiler Sie warnen.

Wenn Ihr Thread die Wartezeit erreicht, geht er seinen Aufrufstapel nach oben, um zu prüfen, ob er etwas anderes tun kann, bis er eine Wartezeit sieht, den Aufrufstapel erneut nach oben geht usw. Sobald alle warten, geht er den Aufrufstapel nach unten und startet Warten Sie untätig, bis der erste abwartende Prozess abgeschlossen ist.

Nachdem der abwartende Prozess abgeschlossen ist, verarbeitet der Thread die Anweisungen nach dem Abwarten weiter, bis er wieder ein Abwarten sieht.

Möglicherweise verarbeitet ein anderer Thread die Anweisungen, die nach dem Warten eingehen, weiter (dies können Sie im Debugger durch Überprüfen der Thread-ID feststellen). Dieser andere Thread hat jedoch das context des ursprünglichen Threads, sodass er so tun kann, als wäre er der ursprüngliche Thread. Keine Notwendigkeit für Mutexe, Semaphoren, IsInvokeRequired (in Winforms) usw. Für Sie scheint es, als ob es einen Thread gibt.

Manchmal muss Ihr Koch etwas tun, das einige Zeit in Anspruch nimmt, ohne untätig zu warten, z. B. Tomaten schneiden. In diesem Fall kann es sinnvoll sein, einen anderen Koch einzustellen und ihn mit dem Schneiden zu beauftragen. In der Zwischenzeit kann Ihr Koch mit den Eiern fortfahren, die gerade gekocht und geschält wurden.

In Bezug auf Computer wäre dies, wenn Sie einige große Berechnungen hätten, ohne auf andere Prozesse zu warten. Beachten Sie den Unterschied, wenn Sie beispielsweise Daten auf die Festplatte schreiben. Sobald Ihr Thread bestellt hat, dass die Daten auf die Festplatte geschrieben werden müssen, würde er normalerweise untätig warten, bis die Daten geschrieben wurden. Dies ist bei großen Berechnungen nicht der Fall.

Sie können den zusätzlichen Koch mit Task.Run einstellen.

async Task<TimeSpan> CalculateSunSet()
{
    // start fetching sunset data. however don't wait for the result yet
    // you've got better things to do:
    Task<SunsetData> taskFetchData = FetchSunsetData();

    // because you are not awaiting your thread will do the following:
    Location location = FetchLocation();

    // now you need the sunset data, start awaiting for the Task:
    SunsetData sunsetData = await taskFetchData;

    // some big calculations are needed, that take 33 seconds,
    // you want to keep your caller responsive, so start a Task
    // this Task will be run by a different thread:
    ask<DateTime> taskBigCalculations = Taks.Run( () => BigCalculations(sunsetData, location);

    // again no await: you are still free to do other things
    ...
    // before returning you need the result of the big calculations.
    // wait until big calculations are finished, keep caller responsive:
    DateTime result = await taskBigCalculations;
    return result;
}
3

Hast du diese Version in Betracht gezogen?

var userTask = _userRepo.GetByUsername(User.Identity.Name);

//Some work that doesn't rely on the user object

user = await _userRepo.UpdateLastAccessed(await userTask, DateTime.Now);

return user;

Dies führt die "Arbeit" aus, während der Benutzer abgerufen wird, hat aber auch alle Vorteile von await, die in Warten auf eine abgeschlossene Aufgabe wie Aufgabe beschrieben sind. Ergebnis?


Wie vorgeschlagen, können Sie auch eine explizitere Version verwenden, um das Ergebnis des Aufrufs im Debugger zu überprüfen.

var userTask = _userRepo.GetByUsername(User.Identity.Name);

//Some work that doesn't rely on the user object

user = await userTask;
user = await _userRepo.UpdateLastAccessed(user, DateTime.Now);

return user;
0
NineBerry