Unterschied zwischen Warten und Weiter mit

119

Kann jemand im folgenden Beispiel erklären, ob awaitund ContinueWithsind oder nicht. Ich versuche zum ersten Mal, TPL zu verwenden und habe die gesamte Dokumentation gelesen, verstehe aber den Unterschied nicht.

Warten Sie :

String webText = await getWebPage(uri);
await parseData(webText);

Weiter mit :

Task<String> webText = new Task<String>(() => getWebPage(uri));
Task continue = webText.ContinueWith((task) =>  parseData(task.Result));
webText.Start();
continue.Wait();

Wird in bestimmten Situationen das eine dem anderen vorgezogen?

Harrison
quelle
3
Wenn Sie den entfernten WaitAnruf in dem zweiten Beispiel dann die beiden Schnipsel wäre (meist) gleichwertig.
Servy
Zu Ihrer Information: Ihre getWebPageMethode kann nicht in beiden Codes verwendet werden. Im ersten Code hat es einen Task<string>Rückgabetyp, während es im zweiten Code einen Rückgabetyp hat string. Ihr Code wird also im Grunde nicht kompiliert. - wenn um genau zu sein.
Royi Namir

Antworten:

101

Im zweiten Code warten Sie synchron auf den Abschluss der Fortsetzung. In der ersten Version kehrt die Methode zum Aufrufer zurück, sobald sie den ersten awaitAusdruck trifft, der noch nicht abgeschlossen ist.

Sie sind sich insofern sehr ähnlich, als sie beide eine Fortsetzung planen, aber sobald der Kontrollfluss noch etwas komplexer wird, awaitführt dies zu viel einfacherem Code. Wie von Servy in den Kommentaren erwähnt, werden beim Warten auf eine Aufgabe außerdem aggregierte Ausnahmen "entpackt", was normalerweise zu einer einfacheren Fehlerbehandlung führt. Mit using awaitwird implizit die Fortsetzung im aufrufenden Kontext geplant (sofern Sie nicht verwenden ConfigureAwait). Es ist nichts, was nicht "manuell" gemacht werden kann, aber es ist viel einfacher, es damit zu machen await.

Ich schlage vor, Sie versuchen, mit beiden eine etwas größere Abfolge von Operationen zu implementieren, awaitund Task.ContinueWith- es kann ein echter Augenöffner sein.

Jon Skeet
quelle
2
Die Fehlerbehandlung zwischen den beiden Snippets ist ebenfalls unterschiedlich. In dieser Hinsicht ist es im Allgemeinen einfacher, mit awaitOver zu arbeiten ContinueWith.
Servy
@Servy: Stimmt, wird etwas hinzufügen.
Jon Skeet
1
Die Planung ist auch ganz anders, dh in welchem ​​Kontext parseDataausgeführt wird.
Stephen Cleary
Wenn Sie sagen, dass die Verwendung von await implizit die Fortsetzung im aufrufenden Kontext plant , können Sie den Nutzen davon erklären und was in der anderen Situation passiert?
Harrison
4
@ Harrison: Stellen Sie sich vor, Sie schreiben eine WinForms-App. Wenn Sie eine asynchrone Methode schreiben, wird standardmäßig der gesamte Code innerhalb der Methode im UI-Thread ausgeführt, da die Fortsetzung dort geplant wird. Wenn Sie nicht angeben, wo die Fortsetzung ausgeführt werden soll, weiß ich nicht, wie die Standardeinstellung lautet, aber sie kann leicht in einem Thread-Pool-Thread ausgeführt werden. An diesem Punkt können Sie nicht auf die Benutzeroberfläche usw. Zugreifen .
Jon Skeet
100

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?

pkt
quelle
Danke, wirklich nette Erklärung
Elger Mensonides
Dies ist ein großartiges Beispiel
Royi Namir