Hier ist die Folge von Codefragmenten, die ich kürzlich verwendet habe, um den Unterschied und verschiedene Probleme bei der Verwendung asynchroner Lösungen zu veranschaulichen.
Angenommen, Sie haben einen Ereignishandler in Ihrer GUI-basierten Anwendung, der viel Zeit in Anspruch nimmt, und möchten ihn daher asynchron machen. Hier ist die synchrone Logik, mit der Sie beginnen:
while (true) {
string result = LoadNextItem().Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
LoadNextItem gibt eine Aufgabe zurück, die schließlich zu einem Ergebnis führt, das Sie überprüfen möchten. Wenn das aktuelle Ergebnis das gesuchte ist, aktualisieren Sie den Wert eines Zählers auf der Benutzeroberfläche und kehren von der Methode zurück. Andernfalls verarbeiten Sie weitere Elemente aus LoadNextItem.
Erste Idee für die asynchrone Version: Verwenden Sie einfach Fortsetzungen! Und lassen Sie uns den Loop-Teil vorerst ignorieren. Ich meine, was könnte möglicherweise schief gehen?
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
});
Großartig, jetzt haben wir eine Methode, die nicht blockiert! Es stürzt stattdessen ab. Alle Aktualisierungen der UI-Steuerelemente sollten im UI-Thread erfolgen, daher müssen Sie dies berücksichtigen. Zum Glück gibt es eine Option, mit der festgelegt werden kann, wie Fortsetzungen geplant werden sollen, und es gibt eine Standardoption für genau dies:
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
},
TaskScheduler.FromCurrentSynchronizationContext());
Großartig, jetzt haben wir eine Methode, die nicht abstürzt! Es schlägt stattdessen lautlos fehl. Fortsetzungen sind separate Aufgaben selbst, deren Status nicht an den der vorherigen Aufgabe gebunden ist. Selbst wenn LoadNextItem fehlerhaft ist, wird dem Aufrufer nur eine Aufgabe angezeigt, die erfolgreich abgeschlossen wurde. Okay, dann geben Sie einfach die Ausnahme weiter, falls es eine gibt:
return LoadNextItem().ContinueWith(t => {
if (t.Exception != null) {
throw t.Exception.InnerException;
}
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
},
TaskScheduler.FromCurrentSynchronizationContext());
Großartig, jetzt funktioniert das tatsächlich. Für einen einzelnen Artikel. Wie wäre es nun mit dieser Schleife? Es stellt sich heraus, dass eine Lösung, die der Logik der ursprünglichen synchronen Version entspricht, ungefähr so aussieht:
Task AsyncLoop() {
return AsyncLoopTask().ContinueWith(t =>
Counter.Value = t.Result,
TaskScheduler.FromCurrentSynchronizationContext());
}
Task<int> AsyncLoopTask() {
var tcs = new TaskCompletionSource<int>();
DoIteration(tcs);
return tcs.Task;
}
void DoIteration(TaskCompletionSource<int> tcs) {
LoadNextItem().ContinueWith(t => {
if (t.Exception != null) {
tcs.TrySetException(t.Exception.InnerException);
} else if (t.Result.Contains("target")) {
tcs.TrySetResult(t.Result.Length);
} else {
DoIteration(tcs);
}});
}
Anstelle der oben genannten Schritte können Sie auch Async verwenden, um dasselbe zu tun:
async Task AsyncLoop() {
while (true) {
string result = await LoadNextItem();
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
}
Das ist jetzt viel schöner, nicht wahr?
Wait
Anruf in dem zweiten Beispiel dann die beiden Schnipsel wäre (meist) gleichwertig.