Kann das letzte Warten durch ein explizites Warten ersetzt werden?
-
11-12-2019 - |
Frage
Ich lerne immer noch async
/await
, also bitte entschuldigen Sie, wenn ich etwas Offensichtliches frage.Betrachten Sie das folgende Beispiel:
class Program {
static void Main(string[] args) {
var result = FooAsync().Result;
Console.WriteLine(result);
}
static async Task<int> FooAsync() {
var t1 = Method1Async();
var t2 = Method2Async();
var result1 = await t1;
var result2 = await t2;
return result1 + result2;
}
static Task<int> Method1Async() {
return Task.Run(
() => {
Thread.Sleep(1000);
return 11;
}
);
}
static Task<int> Method2Async() {
return Task.Run(
() => {
Thread.Sleep(1000);
return 22;
}
);
}
}
Dies verhält sich wie erwartet und gibt „33“ in der Konsole aus.
Wenn ich das ersetze zweite await
mit explizitem Warten...
static async Task<int> FooAsync() {
var t1 = Method1Async();
var t2 = Method2Async();
var result1 = await t1;
var result2 = t2.Result;
return result1 + result2;
}
...Ich scheine das gleiche Verhalten zu haben.
Sind diese beiden Beispiele völlig gleichwertig?
Und wenn sie in diesem Fall gleichwertig sind, gibt es andere Fälle, in denen die letzten ersetzt werden? await
durch ein explizites Warten einen Unterschied machen würde?
Lösung 3
OK, ich glaube, ich habe es herausgefunden, also möchte ich es zusammenfassen, hoffentlich mit einer ausführlicheren Erklärung als die bisher gegebenen Antworten ...
Kurze Antwort
Den zweiten ersetzen await
mit einer expliziten Wartezeit hat keine nennenswerten Auswirkungen auf eine Konsolenanwendung, blockiert jedoch den UI-Thread einer WPF- oder WinForms-Anwendung für die Dauer der Wartezeit.
Außerdem ist die Ausnahmebehandlung etwas anders (wie von Stephen Cleary festgestellt).
Lange Antwort
Kurz gesagt, die await
macht dies:
- Wenn die erwartete Aufgabe bereits abgeschlossen ist, ruft sie einfach ihr Ergebnis ab und fährt fort.
- Wenn nicht, dann Beiträge die Fortsetzung (der Rest der Methode nach dem
await
) zum aktuell Synchronisationskontext, falls vorhanden.Im Wesentlichen,await
versucht uns dorthin zurückzubringen, wo wir angefangen haben.- Wenn kein aktueller Kontext vorhanden ist, wird einfach das Original verwendet TaskScheduler, bei dem es sich normalerweise um einen Thread-Pool handelt.
Der zweite (und der dritte usw.) await
macht das Gleiche.
Da die Konsolenanwendungen normalerweise keinen Synchronisierungskontext haben, werden Fortsetzungen normalerweise vom Thread-Pool verarbeitet, sodass es kein Problem gibt, wenn wir innerhalb der Fortsetzung blockieren.
WinForms oder WPF hingegen verfügen über einen Synchronisierungskontext, der zusätzlich zu ihrer Nachrichtenschleife implementiert ist.Daher, await
Wenn ein Befehl, der auf einem UI-Thread ausgeführt wird, (irgendwann) seine Fortsetzung auch auf dem UI-Thread ausführt.Wenn wir die Fortsetzung blockieren, wird die Nachrichtenschleife blockiert und die Benutzeroberfläche reagiert nicht mehr, bis wir die Blockierung aufheben.OTOH, wenn wir nur await
, werden Fortsetzungen sauber gepostet, um schließlich im UI-Thread ausgeführt zu werden, ohne jemals den UI-Thread zu blockieren.
Im folgenden WinForms-Formular, das eine Schaltfläche und eine Beschriftung enthält, wird verwendet await
sorgt dafür, dass die Benutzeroberfläche jederzeit reagiert (beachten Sie die async
vor dem Click-Handler):
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private async void button1_Click(object sender, EventArgs e) {
var result = await FooAsync();
label1.Text = result.ToString();
}
static async Task<int> FooAsync() {
var t1 = Method1Async();
var t2 = Method2Async();
var result1 = await t1;
var result2 = await t2;
return result1 + result2;
}
static Task<int> Method1Async() {
return Task.Run(
() => {
Thread.Sleep(3000);
return 11;
}
);
}
static Task<int> Method2Async() {
return Task.Run(
() => {
Thread.Sleep(5000);
return 22;
}
);
}
}
Wenn wir den zweiten ersetzen await
In FooAsync
mit t2.Result
, würde es nach dem Klicken auf die Schaltfläche noch etwa 3 Sekunden lang reagieren und dann etwa 2 Sekunden lang einfrieren:
- Die Fortsetzung nach dem ersten
await
wird höflich warten, bis er an der Reihe ist, bis er im UI-Thread eingeplant wird, was danach passieren würdeMethod1Async()
Aufgabe beendet, d.h.nach etwa 3 sekunden, - an welchem Punkt die
t2.Result
wird den UI-Thread grob blockieren, bis derMethod2Async()
Die Aufgabe wird etwa 2 Sekunden später beendet.
Wenn wir das entfernt haben async
vor dem button1_Click
und ersetzte es await
mit FooAsync().Result
es würde zum Stillstand kommen:
- Der UI-Thread würde warten
FooAsync()
Aufgabe zu erledigen, - das darauf warten würde, dass es weitergeht,
- die darauf warten würde, dass der UI-Thread verfügbar wird,
- was nicht der Fall ist, da es durch die blockiert wird
FooAsync().Result
.
Der Artikel „Await-, SynchronizationContext- und Konsolen-Apps“ von Stephen Toub war für mich von unschätzbarem Wert für das Verständnis dieses Themas.
Andere Tipps
Ihre Ersatzversion blockiert den aufrufenden Thread, der auf die Aufgabe wartet, um zu beenden.Es ist schwer, einen sichtbaren Unterschied in einer Console-App so zu sehen, da Sie absichtlich in der Main blockieren, aber sie sind definitiv nicht gleichwertig.
Sie sind nicht gleichwertig.
Task.Result
Blöcke, bis das Ergebnis verfügbar ist.Wie ich auf meinem Blog erkläre, ist dies der Fall kann zu Deadlocks führen wenn Sie eine haben async
Kontext, der exklusiven Zugriff erfordert (z. B. eine Benutzeroberfläche oder eine ASP.NET-App).
Auch, Task.Result
wird alle Ausnahmen einschließen AggregateException
, Daher ist die Fehlerbehandlung schwieriger, wenn Sie synchron blockieren.