Różnica między await i ContinueWith

119

Czy ktoś może wyjaśnić, czy awaiti ContinueWithsą synonimami, czy nie w poniższym przykładzie. Próbuję po raz pierwszy skorzystać z TPL i przeczytałem całą dokumentację, ale nie rozumiem różnicy.

Oczekuj :

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

Kontynuuj z :

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

Czy jeden jest preferowany w określonych sytuacjach?

Harrison
źródło
3
Po wyjęciu z Waitpołączenia w drugim przykładzie następnie dwa fragmenty będzie (w większości) równoważne.
Servy
FYI: Twoja getWebPagemetoda nie może być używana w obu kodach. W pierwszym kodzie ma Task<string>typ zwracany, w drugim stringtyp zwracany. więc w zasadzie twój kod się nie kompiluje. - żeby być precyzyjnym.
Royi Namir

Odpowiedzi:

101

W drugim kodzie synchronicznie czekasz na zakończenie kontynuacji. W pierwszej wersji metoda powróci do obiektu wywołującego, gdy tylko trafi w pierwsze awaitwyrażenie, które nie jest jeszcze zakończone.

Są bardzo podobne pod tym względem, że obaj planują kontynuację, ale gdy tylko przepływ sterowania stanie się nawet nieco skomplikowany, awaitprowadzi do znacznie prostszego kodu. Dodatkowo, jak zauważył Servy w komentarzach, oczekiwanie na zadanie spowoduje „rozpakowanie” zagregowanych wyjątków, co zwykle prowadzi do prostszej obsługi błędów. Użycie również awaitniejawnie zaplanuje kontynuację w kontekście wywołania (chyba że używasz ConfigureAwait). To nic, czego nie można zrobić „ręcznie”, ale o wiele łatwiej to zrobić await.

Sugeruję, abyś spróbował zaimplementować nieco większą sekwencję operacji z obydwoma awaiti Task.ContinueWith- może to być prawdziwy otwieracz oczu.

Jon Skeet
źródło
2
Obsługa błędów między dwoma fragmentami jest również inna; to na ogół łatwiej pracować awaitnad ContinueWithw tym względzie.
Servy
@Servy: True, doda coś wokół tego.
Jon Skeet
1
Harmonogram jest również zupełnie inny, tj. W jakim kontekście jest parseDatawykonywany.
Stephen Cleary
Kiedy mówisz, że użycie await niejawnie zaplanuje kontynuację w kontekście wywołania , czy możesz wyjaśnić korzyści płynące z tego i co się dzieje w innej sytuacji?
Harrison,
4
@Harrison: Wyobraź sobie, że piszesz aplikację WinForms - jeśli napiszesz metodę asynchroniczną, domyślnie cały kod tej metody będzie uruchamiany w wątku interfejsu użytkownika, ponieważ kontynuacja będzie tam zaplanowana. Jeśli nie określisz, gdzie chcesz uruchomić kontynuację, nie wiem, jakie jest ustawienie domyślne, ale może łatwo skończyć się uruchomieniem w wątku puli wątków ... w którym momencie nie możesz uzyskać dostępu do interfejsu użytkownika itp. .
Jon Skeet
100

Oto sekwencja fragmentów kodu, których ostatnio użyłem, aby zilustrować różnicę i różne problemy przy użyciu rozwiązań asynchronicznych.

Załóżmy, że masz program obsługi zdarzeń w aplikacji opartej na GUI, który zajmuje dużo czasu, więc chciałbyś, aby był asynchroniczny. Oto logika synchroniczna, od której zaczynasz:

while (true) {
    string result = LoadNextItem().Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
        break;
    }
}

LoadNextItem zwraca zadanie, które ostatecznie da wynik, który chcesz sprawdzić. Jeśli bieżący wynik jest tym, którego szukasz, aktualizujesz wartość jakiegoś licznika w interfejsie użytkownika i wracasz z metody. W przeciwnym razie kontynuujesz przetwarzanie większej liczby elementów z LoadNextItem.

Pierwszy pomysł na wersję asynchroniczną: po prostu użyj kontynuacji! I na razie zignorujmy część zapętloną. Mam na myśli, co mogłoby się nie udać?

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
});

Świetnie, teraz mamy metodę, która nie blokuje! Zamiast tego ulega awarii. Wszelkie aktualizacje formantów interfejsu użytkownika powinny mieć miejsce w wątku interfejsu użytkownika, więc musisz to uwzględnić. Na szczęście istnieje opcja określenia, w jaki sposób powinny być zaplanowane kontynuacje, i jest domyślna tylko do tego:

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

Świetnie, teraz mamy metodę, która się nie zawiesza! Zamiast tego po cichu zawodzi. Kontynuacje same w sobie są oddzielnymi zadaniami, których status nie jest powiązany ze statusem zadania poprzedzającego. Więc nawet jeśli LoadNextItem zakończy się niepowodzeniem, wywołujący zobaczy tylko zadanie, które zostało pomyślnie zakończone. OK, po prostu przekaż wyjątek, jeśli taki istnieje:

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());

Świetnie, teraz to faktycznie działa. Dla jednej pozycji. A co z tym zapętleniem. Okazuje się, że rozwiązanie odpowiadające logice oryginalnej wersji synchronicznej będzie wyglądało mniej więcej tak:

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);
        }});
}

Lub, zamiast wszystkich powyższych, możesz użyć async, aby zrobić to samo:

async Task AsyncLoop() {
    while (true) {
        string result = await LoadNextItem();
        if (result.Contains("target")) {
            Counter.Value = result.Length;
            break;
        }
    }
}

Teraz jest o wiele ładniej, prawda?

pkt
źródło
Dzięki, naprawdę miłe wyjaśnienie
Elger Mensonides
To świetny przykład
Royi Namir