await Task.Run vs await

await Task.Run vs await

Task.Run kann Posten Sie die zu verarbeitende Operation in einem anderen Thread. Das ist der einzige Unterschied.

Dies kann nützlich sein - zum Beispiel wenn LongProcess nicht wirklich asynchron ist, führt dies dazu, dass der Aufrufer schneller zurückkehrt. Aber für eine wirklich asynchrone Methode macht es keinen Sinn, Task.Run zu verwenden , und es kann zu unnötiger Verschwendung führen.

Seien Sie jedoch vorsichtig, da das Verhalten von Task.Run ändert sich basierend auf der Überlastungsauflösung. In Ihrem Beispiel die Func<Task> Überladung wird gewählt, die (korrekterweise) auf LongProcess wartet beenden. Wenn jedoch ein Delegat verwendet wurde, der keine Aufgabe zurückgibt, Task.Run wird nur bis zum ersten await auf die Ausführung warten (Beachten Sie, dass so TaskFactory.StartNew wird immer benimm dich, also benutze das nicht).


Sehr oft denken die Leute, dass async-await von mehreren Threads durchgeführt wird. Tatsächlich wird alles von einem Thread erledigt.

Was mir sehr geholfen hat, async-await zu verstehen, ist dieses Interview mit Eric Lippert über async-await. Irgendwo in der Mitte vergleicht er async await mit einem Koch, der warten muss, bis etwas Wasser kocht. Anstatt nichts zu tun, schaut er sich um, ob es noch etwas anderes zu tun gibt, als die Zwiebeln zu schneiden. Wenn das erledigt ist und das Wasser immer noch nicht kocht, prüft er, ob es noch etwas zu tun gibt, und so weiter, bis er nichts anderes zu tun hat, als zu warten. In diesem Fall kehrt er zum ersten, worauf er gewartet hat, zurück.

Wenn Ihre Prozedur eine Awaitable-Funktion aufruft, sind wir sicher, dass es irgendwo in dieser Awaitable-Funktion einen Aufruf zu einer Awaitable-Funktion gibt, sonst wäre die Funktion nicht Awaitable. Tatsächlich wird Ihr Compiler Sie warnen, wenn Sie vergessen, irgendwo in Ihrer Awaitable-Funktion zu warten.

Wenn Ihre Awaitable-Funktion die andere Awaitable-Funktion aufruft, tritt der Thread in diese andere Funktion ein und beginnt, die Dinge in dieser Funktion zu erledigen, und geht tiefer in andere Funktionen, bis er auf ein Await trifft.

Anstatt auf die Ergebnisse zu warten, geht der Thread in seinem Call-Stack nach oben, um zu sehen, ob es andere Codeteile gibt, die er verarbeiten kann, bis er ein await sieht. Gehen Sie in der Aufrufliste wieder nach oben, verarbeiten Sie bis zum Warten usw. Sobald alle warten, sucht der Thread nach dem unteren Warten und fährt fort, sobald dies beendet ist.

Dies hat den Vorteil, dass, wenn der Aufrufer Ihrer Awaitable-Funktion das Ergebnis Ihrer Funktion nicht benötigt, aber andere Dinge tun kann, bevor das Ergebnis benötigt wird, diese anderen Dinge vom Thread erledigt werden können, anstatt in Ihrer Funktion zu warten.

Ein Aufruf ohne sofortiges Warten auf das Ergebnis sähe so aus:

private async Task MyFunction()
{
    Task<ReturnType>taskA = SomeFunctionAsync(...)
    // I don't need the result yet, I can do something else
    DoSomethingElse();

    // now I need the result of SomeFunctionAsync, await for it:
    ReturnType result = await TaskA;
    // now you can use object result
}

Beachten Sie, dass in diesem Szenario alles von einem Thread erledigt wird. Solange dein Thread etwas zu tun hat, wird er beschäftigt sein.

Der Link zum Artikel am Ende dieser Antwort erklärt etwas mehr über den Thread-Kontext

Sie werden Awaitable-Funktionen hauptsächlich dort sehen, wo ein anderer Prozess Dinge erledigen muss, während Ihr Thread nur untätig warten muss, bis der andere fertig ist. Beispiele sind das Senden von Daten über das Internet, das Speichern einer Datei, die Kommunikation mit einer Datenbank usw.

Manchmal müssen jedoch einige schwere Berechnungen durchgeführt werden, und Sie möchten, dass Ihr Thread andere Aufgaben ausführen kann, z. B. auf Benutzereingaben reagieren kann. In diesem Fall können Sie eine Awaiable-Aktion starten, als ob Sie eine asynchrone Funktion aufgerufen hätten.

Task<ResultType> LetSomeoneDoHeavyCalculations(...)
{
    DoSomePreparations()
    // start a different thread that does the heavy calculations:
    var myTask = Task.Run( () => DoHeavyCalculations(...))
    // now you are free to do other things
    DoSomethingElse();
    // once you need the result of the HeavyCalculations await for it
    var myResult = await myTask;
    // use myResult
    ...
}

Jetzt führt ein anderer Thread die schweren Berechnungen durch, während Ihr Thread frei ist, andere Dinge zu tun. Sobald es anfängt zu warten, kann Ihr Anrufer Dinge tun, bis er anfängt zu warten. Tatsächlich kann Ihr Thread ziemlich frei auf Benutzereingaben reagieren. Dies wird jedoch nur der Fall sein, wenn alle warten. Während Ihr Thread damit beschäftigt ist, Dinge zu erledigen, kann Ihr Thread nicht auf Benutzereingaben reagieren. Stellen Sie daher immer sicher, dass Sie Task.Run verwenden, wenn Sie glauben, dass Ihr UI-Thread eine ausgelastete Verarbeitung durchführen muss, die einige Zeit in Anspruch nimmt, und dies einem anderen Thread überlassen

Ein weiterer Artikel, der mir geholfen hat:Async-Await von dem brillanten Erklärer Stephen Cleary


Diese Antwort befasst sich mit dem speziellen Fall des Wartens auf eine asynchrone Methode im Ereignishandler einer GUI-Anwendung. In diesem Fall hat der erste Ansatz einen deutlichen Vorteil gegenüber dem zweiten. Bevor wir erklären, warum, schreiben wir die beiden Ansätze so um, dass sie den Kontext dieser Antwort klar widerspiegeln. Das Folgende ist nur für Event-Handler von GUI-Anwendungen relevant.

private async void Button1_Click(object sender, EventArgs args)
{
    await Task.Run(async () => await LongProcessAsync());
}

gegen

private async void Button1_Click(object sender, EventArgs args)
{
    await LongProcessAsync();
}

Ich habe das Suffix Async hinzugefügt im Namen der Methode, um die Richtlinien einzuhalten. Ich habe auch den anonymen Delegaten asynchron gemacht, nur aus Gründen der Lesbarkeit. Der Aufwand für die Erstellung einer Zustandsmaschine ist winzig und wird durch den Wert der klaren Kommunikation dieses Task.Run in den Schatten gestellt gibt ein Task im Promise-Stil zurück , kein Delegierter der alten Schule Task für die Hintergrundverarbeitung von CPU-gebundenen Workloads vorgesehen.

Der Vorteil des ersten Ansatzes besteht darin, dass garantiert wird, dass die Benutzeroberfläche reaktionsfähig bleibt. Der zweite Ansatz bietet keine solche Garantie. Solange Sie die integrierten asynchronen APIs der .NET-Plattform verwenden, ist die Wahrscheinlichkeit, dass die Benutzeroberfläche durch den zweiten Ansatz blockiert wird, ziemlich gering. Schließlich werden diese APIs von Experten implementiert¹. In dem Moment, in dem Sie beginnen, auf Ihren eigenen zu warten async-Methoden sind alle Garantien deaktiviert. Es sei denn, Ihr Vorname ist natürlich Stephen und Ihr Nachname ist Toub oder Cleary. Wenn das nicht der Fall ist, ist es durchaus möglich, dass Sie früher oder später Code wie diesen schreiben werden:

public static async Task LongProcessAsync()
{
    TeenyWeenyInitialization(); // Synchronous
    await SomeBuildInAsyncMethod().ConfigureAwait(false); // Asynchronous
    CalculateAndSave(); // Synchronous
}

Das Problem liegt offensichtlich bei der Methode TeenyWeenyInitialization() . Diese Methode ist synchron und steht vor dem ersten await innerhalb des Hauptteils der async-Methode, sodass nicht darauf gewartet wird. Es wird bei jedem Aufruf von LongProcessAsync() synchron ausgeführt . Folgt man also dem zweiten Ansatz (ohne Task.Run ), die TeenyWeenyInitialization() wird im UI-Thread ausgeführt .

Wie schlimm kann das sein? Die Initialisierung ist schließlich klitzeklein! Nur ein kurzer Ausflug in die Datenbank, um einen Wert zu erhalten, die erste Zeile einer kleinen Textdatei lesen, einen Wert aus der Registrierung abrufen. In ein paar Millisekunden ist alles vorbei. Zu der Zeit, als Sie das Programm geschrieben haben. Auf Ihrem PC. Vor dem Verschieben des Datenordners in ein freigegebenes Laufwerk. Bevor die Datenmenge in der Datenbank riesig wurde.

Aber vielleicht haben Sie Glück und die TeenyWeenyInitialization() bleibt für immer schnell, was ist mit der zweiten synchronen Methode, der CalculateAndSave() ? Dieser kommt nach einem await der so konfiguriert ist, dass er den Kontext nicht erfasst, sodass er in einem Thread-Pool-Thread ausgeführt wird. Es sollte niemals im UI-Thread laufen, oder? Falsch. Es hängt vom Task ab zurückgegeben von SomeBuildInAsyncMethod() . Wenn der Task abgeschlossen ist, findet kein Threadwechsel statt und der CalculateAndSave() wird auf demselben Thread ausgeführt, der die Methode aufgerufen hat. Wenn Sie dem zweiten Ansatz folgen, ist dies der UI-Thread . Sie werden möglicherweise nie einen Fall erleben, in dem der SomeBuildInAsyncMethod() hat ein abgeschlossenes Task zurückgegeben in Ihrer Entwicklungsumgebung, aber die Produktionsumgebung kann auf schwer vorhersehbare Weise anders sein.

Es ist unangenehm, eine Anwendung zu haben, die schlecht funktioniert. Eine Anwendung zu haben, die und schlecht funktioniert friert die UI ist noch schlimmer. Willst du es wirklich riskieren? Falls nicht, verwenden Sie bitte immer Task.Run(async in Ihren Event-Handlern. Vor allem, wenn Sie auf selbst codierte Methoden warten!

¹ Haftungsausschluss, einige integrierte asynchrone APIs sind nicht richtig implementiert.

Wichtig: Der Task.Run führt den bereitgestellten asynchronen Delegaten auf einem ThreadPool aus Thread, daher ist es erforderlich, dass der LongProcessAsync hat keine Affinität zum UI-Thread. Wenn es um die Interaktion mit UI-Steuerelementen geht, dann Task.Run ist keine Option. Vielen Dank an @Zmaster für den Hinweis auf diese wichtige Subtilität in den Kommentaren.