Jaki jest cel „powrotu oczekuj” w C #?

251

Czy istnieje jakikolwiek scenariusz, w którym pisanie metodę tak:

public async Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return await DoAnotherThingAsync();
}

zamiast tego:

public Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return DoAnotherThingAsync();
}

miałoby sens?

Po co używać return awaitkonstruktora, skoro można bezpośrednio powrócić Task<T>z wewnętrznego DoAnotherThingAsync()wywołania?

Widzę kod return awaitw tak wielu miejscach, myślę, że mogłem coś przeoczyć. Ale o ile rozumiem, nieużywanie słów kluczowych async / czekaj w tym przypadku i bezpośrednie zwrócenie zadania byłoby funkcjonalnie równoważne. Po co dodawać dodatkowy narzut dodatkowej awaitwarstwy?

TX_
źródło
2
Myślę, że jedynym powodem, dla którego to widzisz, jest to, że ludzie uczą się naśladując i ogólnie (jeśli nie potrzebują) używają najprostszego rozwiązania, jakie mogą znaleźć. Więc ludzie widzą ten kod, używają tego kodu, widzą, że działa i odtąd dla nich jest to właściwy sposób na zrobienie tego ... Nie ma sensu czekać w takim przypadku
Fabio Marcolini
7
Jest co najmniej jedna ważna różnica: propagacja wyjątków .
noseratio
1
Ja też tego nie rozumiem, nie mogę w ogóle zrozumieć tej całej koncepcji, nie ma to żadnego sensu. Z tego, czego się nauczyłem, jeśli metoda ma typ zwracany, MUSI mieć zwrotne słowo kluczowe, czy nie są to reguły języka C #?
monstro
@monstro pytanie OP ma jednak instrukcję return?
David Klempfner,

Odpowiedzi:

189

W jednym przypadku podstępne gdy returnw normalnym sposobem i return awaitw asyncmetodzie zachowują się inaczej: w połączeniu z using(lub, bardziej ogólnie, każdy return awaitw trybloku).

Rozważ te dwie wersje metody:

Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return foo.DoAnotherThingAsync();
    }
}

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

Pierwsza metoda przedmiot jak tylko powróci metodzie, która prawdopodobnie na długo zanim faktycznie kończy. Oznacza to, że pierwsza wersja jest prawdopodobnie wadliwa (ponieważ zostanie usunięta zbyt wcześnie), podczas gdy druga wersja będzie działać poprawnie.Dispose()FooDoAnotherThingAsync()Foo

svick
źródło
4
Dla kompletności w pierwszym przypadku powinieneś powrócićfoo.DoAnotherThingAsync().ContinueWith(_ => foo.Dispose());
ghord
7
@ghord To nie zadziała, Dispose()zwraca void. Potrzebujesz czegoś takiego return foo.DoAnotherThingAsync().ContinueWith(t -> { foo.Dispose(); return t.Result; });. Ale nie wiem, dlaczego miałbyś to zrobić, skoro możesz skorzystać z drugiej opcji.
svick
1
@svick Masz rację, powinno być bardziej podobne do { var task = DoAnotherThingAsync(); task.ContinueWith(_ => foo.Dispose()); return task; }. Przypadek użycia jest dość prosty: jeśli korzystasz z .NET 4.0 (jak większość), możesz nadal pisać kod asynchroniczny w ten sposób, który będzie działał ładnie z aplikacji 4.5.
ghord
2
@ghord Jeśli korzystasz z .Net 4.0 i chcesz pisać kod asynchroniczny, prawdopodobnie powinieneś użyć Microsoft.Bcl.Async . A twój kod usuwa się Foodopiero po zakończeniu zwrotu Task, co mi się nie podoba, ponieważ niepotrzebnie wprowadza współbieżność.
svick,
1
@svick Twój kod czeka również na zakończenie zadania. Ponadto Microsoft.Bcl.Async jest dla mnie bezużyteczny ze względu na zależność od KB2468871 i konflikty podczas używania asynchronicznej bazy kodu .NET 4.0 z odpowiednim kodem asynchronicznym 4.5.
ghord
93

Jeśli nie potrzebujesz async(tzn. Możesz zwrócić Taskbezpośrednio), nie używaj async.

Jest kilka sytuacji, w których return awaitjest to przydatne, na przykład jeśli masz do wykonania dwie operacje asynchroniczne:

var intermediate = await FirstAsync();
return await SecondAwait(intermediate);

Więcej informacji na temat asyncwydajności można znaleźć w artykule MSDN Stephen Toub i wideo na ten temat.

Aktualizacja: Napisałem post na blogu, który zawiera dużo więcej szczegółów.

Stephen Cleary
źródło
13
Czy możesz dodać wyjaśnienie, dlaczego awaitjest to przydatne w drugim przypadku? Dlaczego nie return SecondAwait(intermediate);?
Matt Smith
2
Mam takie samo pytanie jak Matt, czy nie return SecondAwait(intermediate);osiągnąłby również celu w tym przypadku? Myślę, że return awaittutaj również jest zbędny ...
TX_
23
@MattSmith To się nie skompiluje. Jeśli chcesz użyć awaitw pierwszym wierszu, musisz użyć go również w drugim wierszu.
svick 30.09.13
2
@ cateyes Nie jestem pewien, co oznacza „narzut wprowadzony przez ich równoległe połączenie”, ale asyncwersja zużywa mniej zasobów (wątków) niż wersja synchroniczna.
sick
4
@TomLint To naprawdę się nie kompiluje. Zakładając, że typem zwracanym SecondAwaitjest „ciąg, komunikat o błędzie brzmi:„ CS4016: Ponieważ jest to metoda asynchroniczna, wyrażenie zwrotne musi być typu „ciąg”, a nie „Zadanie <ciąg>”.
svick,
23

Jedynym powodem, dla którego chcesz to zrobić, jest to, że istnieje jakiś inny awaitkod we wcześniejszym kodzie lub jeśli w jakiś sposób manipulujesz wynikiem przed jego zwróceniem. Innym sposobem, w jaki może się to zdarzyć, jest try/catchzmiana sposobu obsługi wyjątków. Jeśli nie robisz nic z tego, masz rację, nie ma powodu, aby dodawać narzut związany z tworzeniem metody async.

Servy
źródło
4
Podobnie jak w przypadku odpowiedzi Stephena, nie rozumiem, dlaczego byłoby return awaitto konieczne (zamiast po prostu zwracać zadanie wywołania dziecka), nawet jeśli we wcześniejszym kodzie jest jeszcze coś innego . Czy możesz podać wyjaśnienie?
TX_
10
@TX_ Jeśli chcesz usunąć, asyncto jak czekasz na pierwsze zadanie? Musisz oznaczyć metodę tak async, jakbyś chciał użyć jakichkolwiek oczekujących. Jeśli metoda jest oznaczona jako asynci masz awaitwcześniejszy kod, musisz wykonać awaitdrugą operację asynchroniczną, aby była poprawnego typu. Jeśli właśnie go usunąłeś await, kompilacja nie byłaby możliwa, ponieważ zwracana wartość nie byłaby odpowiedniego typu. Ponieważ metoda jest asyncwynikiem, wynik jest zawsze zawinięty w zadanie.
Servy
11
@Noseratio Wypróbuj dwa. Pierwsza kompilacja. Drugi nie. Komunikat o błędzie poinformuje o problemie. Nie zwrócisz odpowiedniego typu. Gdy w asyncmetodzie nie zwrócisz zadania, zwrócisz wynik zadania, które zostanie następnie zapakowane.
Servy
3
@Servy, oczywiście - masz rację. W tym drugim przypadku powrócilibyśmy Task<Type>jawnie, podczas gdy asyncdyktuje powrót Type(w który zmieniłby się sam kompilator Task<Type>).
noseratio
3
@Itsik Cóż, na pewno asyncjest tylko cukrem syntaktycznym do jawnego łączenia kontynuacji. Nie musisz async nic robić, ale w przypadku niemal każdej trywialnej operacji asynchronicznej znacznie łatwiej jest pracować. Na przykład podany kod nie propaguje błędów tak, jak tego oczekujesz, a robienie tego poprawnie nawet w bardziej skomplikowanych sytuacjach staje się znacznie trudniejsze. Chociaż nigdy nie potrzebujesz async , sytuacje, które opisuję, są wartości dodanej do korzystania z nich.
Servy
17

Innym przypadkiem, który może wymagać oczekiwania na wynik, jest ten:

async Task<IFoo> GetIFooAsync()
{
    return await GetFooAsync();
}

async Task<Foo> GetFooAsync()
{
    var foo = await CreateFooAsync();
    await foo.InitializeAsync();
    return foo;
}

W tym przypadku, GetIFooAsync() należy poczekać na wynik, GetFooAsyncponieważ typ Tjest różny dla obu metod i Task<Foo>nie można go bezpośrednio przypisać Task<IFoo>. Ale jeśli czekasz na wynik, staje się to,Foo co można bezpośrednio przypisać IFoo. Następnie metoda asynchroniczna po prostu ponownie pakuje wynik wewnątrz Task<IFoo>i od razu.

Andrew Arnott
źródło
1
Zgadzam się, to jest naprawdę denerwujące - uważam, że podstawową przyczyną Task<>jest niezmienność.
StuartLC
7

Uczynienie asynchronicznej prostej metody „thunk” tworzy w pamięci maszynę stanu asynchronicznego, a nie asynchroniczną. Chociaż może to często wskazywać na używanie wersji niesynchronicznej, ponieważ jest bardziej wydajna (co jest prawdą), oznacza to również, że w przypadku zawieszenia nie ma dowodów na to, że ta metoda jest zaangażowana w „stos powrotu / kontynuacji” co czasami utrudnia zrozumienie zawieszenia.

Więc tak, gdy perf nie jest krytyczny (a zwykle nie jest), wyrzucę asynchronię na wszystkie te zwarte metody, dzięki czemu będę mieć maszynę stanu asynchronicznego, która pomoże mi zdiagnozować później, a także, aby upewnić się, że jeśli te metody thunk ewoluują z czasem, z pewnością zwrócą błędne zadania zamiast rzucać.

Andrew Arnott
źródło
6

Jeśli nie użyjesz return, możesz zepsuć ślad stosu podczas debugowania lub gdy jest on drukowany w dziennikach wyjątków.

Po zwróceniu zadania metoda spełniła swój cel i nie znajduje się na stosie wywołań. Podczas używania return awaitpozostawiasz go na stosie połączeń.

Na przykład:

Stos wywołań przy użyciu Oczekiwanie: Oczekiwanie na zadanie od B => B Oczekiwanie na zadanie od C

Stos wywołań, gdy nie jest używany, oczekuje: oczekuje na zadanie z C, które B zwróciło.

haimb
źródło
2

To mnie też myli i wydaje mi się, że poprzednie odpowiedzi pomijały twoje aktualne pytanie:

Po co używać komendy return Oczekiwanie na konstrukcję, skoro można bezpośrednio zwrócić zadanie z wewnętrznego wywołania DoAnotherThingAsync ()?

Cóż, czasami tak naprawdę chcesz Task<SomeType>, ale przez większość czasu naprawdę chcesz instancję SomeType, czyli wynik z zadania.

Z Twojego kodu:

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

Osoba niezaznajomiona ze składnią (na przykład ja) może pomyśleć, że ta metoda powinna zwrócić a Task<SomeResult>, ale ponieważ jest oznaczona async, oznacza to, że jej rzeczywistym typem zwrotu jest SomeResult. Jeśli po prostu użyjesz return foo.DoAnotherThingAsync(), zwrócisz zadanie, którego nie można skompilować. Prawidłowym sposobem jest zwrócenie wyniku zadania, więcreturn await .

heltonbiker
źródło
1
„faktyczny typ zwrotu”. Co? async / await nie zmienia typów zwrotów. W twoim przykładzie var task = DoSomethingAsync();dałbyś zadanie, a nieT
But
2
@Buty Nie jestem pewien, czy dobrze to zrozumiałem async/await. O ile mi wiadomo Task task = DoSomethingAsync(), pókiSomething something = await DoSomethingAsync() zarówno do pracy. Pierwszy daje właściwe zadanie, a drugi, ze względu na awaitsłowo kluczowe, daje wynik zadania po jego zakończeniu. Mógłbym na przykład mieć Task task = DoSomethingAsync(); Something something = await task;.
heltonbiker