Ich habe 3 Aufgaben:
private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}
Sie müssen alle ausgeführt werden, bevor mein Code fortgesetzt werden kann, und ich benötige auch die Ergebnisse von jedem. Keines der Ergebnisse hat etwas miteinander gemeinsam
Wie rufe ich an und warte, bis die 3 Aufgaben abgeschlossen sind, und erhalte dann die Ergebnisse?
c#
.net
async-await
task-parallel-library
.net-4.5
Ian Vink
quelle
quelle
Antworten:
Nach der Verwendung
WhenAll
können Sie die Ergebnisse einzeln herausziehen mitawait
:Sie können auch verwenden
Task.Result
(da Sie zu diesem Zeitpunkt wissen, dass alle erfolgreich abgeschlossen wurden). Ich empfehle jedoch die Verwendung,await
da dies eindeutig korrekt ist undResult
in anderen Szenarien Probleme verursachen kann.quelle
WhenAll
; Durch die Wartezeiten wird sichergestellt, dass Sie die drei späteren Aufgaben erst dann erledigen, wenn alle Aufgaben erledigt sind.Task.WhenAll()
ermöglicht das Ausführen der Aufgabe im parallelen Modus. Ich kann nicht verstehen, warum @Servy vorgeschlagen hat, es zu entfernen. Ohne die werdenWhenAll
sie einzeln ausgeführtcatTask
läuft bereits, wenn es zurückkommtFeedCat
. Beide Ansätze funktionieren also - die einzige Frage ist, ob Sie sie einzelnawait
oder alle zusammen verwenden möchten . Die Fehlerbehandlung unterscheidet sich geringfügig - wenn Sie sie verwendenTask.WhenAll
, werdenawait
sie alle verwendet, auch wenn einer von ihnen vorzeitig ausfällt.WhenAll
hat keinen Einfluss darauf, wann oder wie die Vorgänge ausgeführt werden. Es besteht nur die Möglichkeit, die Beobachtung der Ergebnisse zu beeinflussen. In diesem speziellen Fall besteht der einzige Unterschied darin, dass ein Fehler in einer der ersten beiden Methoden dazu führen würde, dass die Ausnahme in meinem Aufrufstapel früher in meiner Methode als in Stephens ausgelöst wird (obwohl immer derselbe Fehler ausgelöst wird, falls vorhanden ).Nur
await
die drei Aufgaben getrennt, nachdem Sie alle gestartet haben.quelle
Task.WhenAll
Änderungen ändert buchstäblich nichts am Verhalten des Programms in irgendeiner beobachtbaren Weise. Es ist ein rein redundanter Methodenaufruf. Sie können es gerne als ästhetische Wahl hinzufügen, wenn Sie möchten, aber es ändert nichts an der Funktionsweise des Codes. Die Ausführungszeit des Codes ist mit oder ohne diesen Methodenaufruf identisch (technisch gesehen ist der Aufwand für den Aufruf sehr geringWhenAll
, sollte aber vernachlässigbar sein), sodass diese Version nur geringfügig länger ausgeführt werden kann als diese Version.WhenAll
ist eine rein ästhetische Veränderung. Der einzige beobachtbare Unterschied im Verhalten besteht darin, ob Sie auf den Abschluss späterer Aufgaben warten, wenn eine frühere Aufgabe fehlerhaft ist, was normalerweise nicht erforderlich ist. Wenn Sie den zahlreichen Erklärungen nicht glauben, warum Ihre Aussage nicht wahr ist, können Sie den Code einfach selbst ausführen und feststellen, dass er nicht wahr ist.Wenn Sie C # 7 verwenden, können Sie eine praktische Wrapper-Methode wie diese verwenden ...
... um eine bequeme Syntax wie diese zu aktivieren, wenn Sie auf mehrere Aufgaben mit unterschiedlichen Rückgabetypen warten möchten. Sie müssten natürlich mehrere Überladungen vornehmen, damit eine unterschiedliche Anzahl von Aufgaben auf Sie wartet.
In der Antwort von Marc Gravell finden Sie jedoch einige Optimierungen in Bezug auf ValueTask und bereits abgeschlossene Aufgaben, wenn Sie dieses Beispiel in etwas Reales verwandeln möchten.
quelle
Task.WhenAll()
gibt kein Tupel zurück. Eine wird aus denResult
Eigenschaften der bereitgestellten Aufgaben erstellt, nachdem die von zurückgegebene AufgabeTask.WhenAll()
abgeschlossen ist..Result
Anrufe gemäß Stephens Argumentation zu ersetzen , um zu vermeiden, dass andere die schlechte Praxis fortsetzen, indem Sie Ihr Beispiel kopieren.Gegeben drei Aufgaben -
FeedCat()
,SellHouse()
undBuyCar()
gibt es zwei interessante Fälle: Entweder sie alle vollständig synchron (aus irgendeinem Grund, vielleicht das Caching oder ein Fehler), oder sie es nicht tun.Nehmen wir an, wir haben aus der Frage:
Ein einfacher Ansatz wäre nun:
aber ... das ist nicht bequem für die Verarbeitung der Ergebnisse; Das möchten wir normalerweise
await
:Dies verursacht jedoch viel Overhead und weist verschiedene Arrays (einschließlich des
params Task[]
Arrays) und Listen (intern) zu. Es funktioniert, aber es ist nicht großartig, IMO. In vielerlei Hinsicht ist es einfacher , eineasync
Operation zu verwenden, und zwarawait
jeweils nacheinander:Im Gegensatz zu einigen der obigen Kommentare macht die Verwendung von
await
anstelle von keinen Unterschied für die Ausführung der Aufgaben (gleichzeitig, nacheinander usw.). Auf höchster Ebene vor einer guten Compiler-Unterstützung für / und war nützlich, wenn diese Dinge nicht existierten . Dies ist auch nützlich, wenn Sie eine beliebige Reihe von Aufgaben anstelle von drei diskreten Aufgaben haben.Task.WhenAll
Task.WhenAll
async
await
Aber: Wir haben immer noch das Problem, dass
async
/await
viel Compiler-Rauschen für die Fortsetzung erzeugt. Ist es wahrscheinlich , dass die Aufgaben ist vielleicht tatsächlich synchron abgeschlossen hat , dann können wir diese optimieren , indem sie mit einem asynchronen Rückfall in einem synchronen Pfad Aufbau:Dieser Ansatz "Synchronisierungspfad mit asynchronem Fallback" wird immer häufiger verwendet, insbesondere bei Hochleistungscode, bei dem synchrone Abschlüsse relativ häufig sind. Beachten Sie, dass es überhaupt nicht hilft, wenn die Fertigstellung immer wirklich asynchron ist.
Zusätzliche Dinge, die hier gelten:
Mit dem aktuellen C # wird ein allgemeines Muster für die
async
Fallback-Methode verwendet, die üblicherweise als lokale Funktion implementiert wird:lieber
ValueTask<T>
,Task<T>
wenn es eine gute Chance gibt, dass die Dinge jemals vollständig synchron mit vielen verschiedenen Rückgabewerten sind:wenn möglich, lieber
IsCompletedSuccessfully
zuStatus == TaskStatus.RanToCompletion
; Dies gibt es jetzt in .NET Core fürTask
und überall fürValueTask<T>
quelle
Task
wenn sie alle fertig sind, ohne die Ergebnisse zu verwenden.await
die "bessere" Ausnahmesemantik erhalten, unter der Annahme, dass Ausnahmen selten, aber sinnvoll sindSie können sie in Aufgaben speichern und dann auf alle warten:
quelle
var catTask = FeedCat()
die Funktion nicht ausFeedCat()
und speichert das ErgebniscatTask
, um dasawait Task.WhenAll()
Teil unbrauchbar zu machen, da die Methode bereits ausgeführt wurde?Wenn Sie versuchen, alle Fehler zu protokollieren, stellen Sie sicher, dass Sie die Zeile Task.WhenAll in Ihrem Code beibehalten. Viele Kommentare deuten darauf hin, dass Sie sie entfernen und auf einzelne Aufgaben warten können. Task.WhenAll ist wirklich wichtig für die Fehlerbehandlung. Ohne diese Zeile lassen Sie Ihren Code möglicherweise für unbeobachtete Ausnahmen offen.
Stellen Sie sich vor, FeedCat löst eine Ausnahme im folgenden Code aus:
In diesem Fall werden Sie weder auf houseTask noch auf carTask warten. Hier gibt es 3 mögliche Szenarien:
SellHouse wurde bereits erfolgreich abgeschlossen, als FeedCat fehlschlug. In diesem Fall geht es dir gut.
SellHouse ist nicht vollständig und schlägt mit Ausnahme irgendwann fehl. Eine Ausnahme wird nicht beachtet und im Finalizer-Thread erneut ausgelöst.
SellHouse ist nicht vollständig und enthält Wartezeiten. Falls Ihr Code in ASP.NET ausgeführt wird, schlägt SellHouse fehl, sobald einige der darin enthaltenen Wartezeiten abgeschlossen sind. Dies geschieht, weil Sie im Grunde genommen einen Fire & Forget-Anruf getätigt haben und der Synchronisationskontext verloren gegangen ist, sobald FeedCat fehlgeschlagen ist.
Hier ist ein Fehler, den Sie für Fall (3) erhalten:
In Fall (2) wird ein ähnlicher Fehler angezeigt, jedoch mit dem ursprünglichen Ausnahmestapel-Trace.
Für .NET 4.0 und höher können Sie nicht beobachtete Ausnahmen mit TaskScheduler.UnobservedTaskException abfangen. Für .NET 4.5 und höher werden nicht beobachtete Ausnahmen standardmäßig verschluckt. Für .NET 4.0 stürzt eine nicht beobachtete Ausnahme Ihren Prozess ab.
Weitere Details finden Sie hier: Behandlung von Aufgabenausnahmen in .NET 4.5
quelle
Sie können
Task.WhenAll
wie erwähnt oder verwendenTask.WaitAll
, je nachdem, ob der Thread warten soll. Schauen Sie sich den Link an, um eine Erklärung für beide zu erhalten.WaitAll vs WhenAll
quelle
Verwenden Sie
Task.WhenAll
und warten Sie dann auf die Ergebnisse:quelle
Vorwärtswarnung
Nur ein kurzer Überblick für diejenigen, die diesen und ähnliche Threads besuchen und nach einer Möglichkeit suchen, EntityFramework mithilfe des Tool-Sets async + await + task zu parallelisieren : Das hier gezeigte Muster ist solide, wenn es jedoch um die spezielle Schneeflocke von EF geht, werden Sie es nicht tun Erzielen Sie eine parallele Ausführung, es sei denn, Sie verwenden eine separate (neue) Datenbankkontextinstanz in jedem beteiligten * Async () -Aufruf.
Diese Art von Dingen ist aufgrund inhärenter Designbeschränkungen von ef-db-Kontexten erforderlich, die es verbieten, mehrere Abfragen parallel in derselben ef-db-Kontextinstanz auszuführen.
Wenn Sie die bereits gegebenen Antworten nutzen, können Sie auf diese Weise sicherstellen, dass Sie alle Werte erfassen, auch wenn eine oder mehrere der Aufgaben zu einer Ausnahme führen:
Eine alternative Implementierung mit mehr oder weniger denselben Leistungsmerkmalen könnte sein:
quelle
Wenn Sie auf Cat zugreifen möchten, gehen Sie folgendermaßen vor:
Dies ist sehr einfach und sehr nützlich, es besteht keine Notwendigkeit, eine komplexe Lösung zu suchen.
quelle
dynamic
ist der Teufel. Es ist für kniffliges COM-Interop und dergleichen gedacht und sollte nicht in Situationen verwendet werden, in denen es nicht unbedingt benötigt wird. Besonders wenn Sie Wert auf Leistung legen. Oder geben Sie Sicherheit ein. Oder Refactoring. Oder das Debuggen.