Was ist der Zweck von "Rückkehr warten" in C #?

251

Gibt es ein Szenario, in dem eine solche Schreibmethode angewendet wird:

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

an Stelle von:

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

würde Sinn machen?

Warum return awaitKonstrukt verwenden, wenn Sie direkt Task<T>vom inneren DoAnotherThingAsync()Aufruf zurückkehren können?

Ich sehe Code mit return awaitan so vielen Stellen, dass ich glaube, ich hätte etwas verpasst. Soweit ich weiß, wäre es jedoch funktional gleichwertig, in diesem Fall keine asynchronen / wartenden Schlüsselwörter zu verwenden und die Aufgabe direkt zurückzugeben. Warum zusätzlichen Aufwand für zusätzliche awaitEbene hinzufügen ?

TX_
quelle
2
Ich denke, der einzige Grund, warum Sie dies sehen, ist, dass Menschen durch Nachahmung lernen und im Allgemeinen (wenn sie es nicht brauchen) die einfachste Lösung verwenden, die sie finden können. Die Leute sehen diesen Code, verwenden diesen Code, sie sehen, dass er funktioniert, und von nun an ist das für sie der richtige Weg ... Es ist in diesem Fall
sinnlos, darauf
7
Es gibt mindestens einen wichtigen Unterschied: die Weitergabe von Ausnahmen .
Noseratio
1
Ich verstehe es auch nicht, kann dieses ganze Konzept überhaupt nicht verstehen, macht keinen Sinn. Nach dem, was ich gelernt habe, wenn eine Methode einen Rückgabetyp hat, MUSS sie ein Rückgabeschlüsselwort haben, sind es nicht die Regeln der C # -Sprache?
Monstro
@monstro hat die Frage des OP die return-Anweisung?
David Klempfner

Antworten:

189

Es gibt einen Fall , wenn sneaky returnin normalen Verfahren und return awaitin asyncVerfahren , verhalten sich anders: in Kombination mit using(oder, allgemeiner, jeder return awaitin einem tryBlock).

Betrachten Sie diese beiden Versionen einer Methode:

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

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

Bei der ersten Methode wird Dispose()das FooObjekt ausgeführt, sobald die DoAnotherThingAsync()Methode zurückgegeben wird. Dies ist wahrscheinlich lange bevor sie tatsächlich abgeschlossen ist. Dies bedeutet, dass die erste Version wahrscheinlich fehlerhaft ist (weil Foosie zu früh entsorgt wird), während die zweite Version einwandfrei funktioniert.

svick
quelle
4
Der Vollständigkeit foo.DoAnotherThingAsync().ContinueWith(_ => foo.Dispose());
halber
7
@ghord Das würde nicht funktionieren, Dispose()kehrt zurück void. Du würdest so etwas brauchen return foo.DoAnotherThingAsync().ContinueWith(t -> { foo.Dispose(); return t.Result; });. Aber ich weiß nicht, warum Sie das tun sollten, wenn Sie die zweite Option verwenden können.
Svick
1
@svick Du hast recht, es sollte eher so sein wie { var task = DoAnotherThingAsync(); task.ContinueWith(_ => foo.Dispose()); return task; }. Der Anwendungsfall ist ziemlich einfach: Wenn Sie (wie die meisten) mit .NET 4.0 arbeiten, können Sie auf diese Weise immer noch asynchronen Code schreiben, der von 4.5-Apps aus gut aufgerufen wird.
Ghord
2
@ghord Wenn Sie mit .NET 4.0 arbeiten und asynchronen Code schreiben möchten, sollten Sie wahrscheinlich Microsoft.Bcl.Async verwenden . Und Ihr Code wird Fooerst nach Abschluss der Rückgabe entsorgt Task, was mir nicht gefällt, da dadurch unnötigerweise Parallelität entsteht.
Svick
1
@svick Ihr Code wartet, bis auch die Aufgabe beendet ist. Außerdem ist Microsoft.Bcl.Async für mich aufgrund der Abhängigkeit von KB2468871 und Konflikten bei Verwendung der asynchronen .NET 4.0-Codebasis mit dem richtigen 4.5-asynchronen Code unbrauchbar.
Ghord
93

Wenn Sie nicht benötigen async(dh Sie können die Taskdirekt zurückgeben), dann verwenden Sie nicht async.

Es gibt Situationen, in denen dies return awaitnützlich ist, z. B. wenn Sie zwei asynchrone Vorgänge ausführen müssen:

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

Weitere Informationen zur asyncLeistung finden Sie im MSDN-Artikel und im Video von Stephen Toub zum Thema.

Update: Ich habe einen Blog-Beitrag geschrieben , der viel detaillierter ist.

Stephen Cleary
quelle
13
Können Sie eine Erklärung hinzufügen, warum das awaitim zweiten Fall nützlich ist? Warum nicht return SecondAwait(intermediate);?
Matt Smith
2
Ich habe die gleiche Frage wie Matt, würde ich return SecondAwait(intermediate);das Ziel auch in diesem Fall nicht erreichen? Ich denke, return awaitist auch hier überflüssig ...
TX_
23
@ MattSmith Das würde nicht kompiliert werden. Wenn Sie awaitin der ersten Zeile verwenden möchten , müssen Sie es auch in der zweiten Zeile verwenden.
Svick
2
@cateyes Ich bin mir nicht sicher, was "Overhead durch Parallelisierung" bedeutet, aber die asyncVersion verbraucht weniger Ressourcen (Threads) als Ihre synchrone Version.
Svick
4
@ TomLint Es wird wirklich nicht kompiliert. Angenommen, der Rückgabetyp von SecondAwaitist "string", lautet die Fehlermeldung: "CS4016: Da dies eine asynchrone Methode ist, muss der Rückgabeausdruck vom Typ" string "und nicht vom Typ" Task <string> "sein.
Svick
23

Der einzige Grund, warum Sie dies tun möchten, ist, wenn awaitder frühere Code einen anderen enthält oder wenn Sie das Ergebnis auf irgendeine Weise manipulieren, bevor Sie es zurückgeben. Eine andere Möglichkeit besteht darin, die try/catchBehandlung von Ausnahmen zu ändern. Wenn Sie nichts davon tun, haben Sie Recht, es gibt keinen Grund, den Aufwand für die Erstellung der Methode zu erhöhen async.

Servieren
quelle
4
Wie bei Stephens Antwort verstehe ich nicht, warum return awaitdies notwendig wäre (anstatt nur die Aufgabe des Kinderaufrufs zurückzugeben), selbst wenn im früheren Code noch etwas anderes erwartet wird . Könnten Sie bitte eine Erklärung geben?
TX_
10
@TX_ Wenn Sie entfernen möchten, asyncwie würden Sie dann auf die erste Aufgabe warten? Sie müssen die Methode markieren , wie asyncwenn Sie verwenden möchten , keine erwartet. Wenn die Methode als markiert ist asyncund Sie einen awaitfrüheren Code haben, müssen Sie awaitdie zweite asynchrone Operation ausführen, damit sie vom richtigen Typ ist. Wenn Sie es gerade entfernt haben, awaitwird es nicht kompiliert, da der Rückgabewert nicht vom richtigen Typ ist. Da die Methode asyncdas Ergebnis ist, wird es immer in eine Aufgabe eingeschlossen.
Servy
11
@Noseratio Probieren Sie die beiden. Der erste kompiliert. Der zweite nicht. Die Fehlermeldung informiert Sie über das Problem. Sie werden nicht den richtigen Typ zurückgeben. Wenn Sie in einer asyncMethode keine Aufgabe zurückgeben, geben Sie das Ergebnis der Aufgabe zurück, das dann umbrochen wird.
Servy
3
@Servy natürlich - du hast recht. Im letzteren Fall würden wir Task<Type>explizit zurückkehren, während wir asyncdiktieren, zurückzukehren Type(was der Compiler selbst werden würde Task<Type>).
Noseratio
3
@Itsik Na klar, asyncist nur syntaktischer Zucker für die explizite Verkabelung von Fortsetzungen. Sie müssen nicht brauchen async , etwas zu tun, aber wenn fast jeden nicht-trivialen asynchronen Betrieb zu tun , es ist dramatisch mit leichter Arbeit. Zum Beispiel verbreitet der von Ihnen bereitgestellte Code Fehler nicht so, wie Sie es möchten, und dies in noch komplexeren Situationen richtig zu machen, wird viel schwieriger. Während Sie nie brauchen async , sind die Situationen, die ich beschreibe, in denen es einen Mehrwert bringt, es zu nutzen.
Servy
17

Ein weiterer Fall, den Sie möglicherweise benötigen, um auf das Ergebnis zu warten, ist der folgende:

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

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

In diesem Fall GetIFooAsync()muss das Ergebnis abwarten, GetFooAsyncda der Typ von Tzwischen den beiden Methoden unterschiedlich ist und Task<Foo>nicht direkt zugeordnet werden kann Task<IFoo>. Aber wenn Sie das Ergebnis warten, wird es nur , Foodas ist direkt zuordenbar IFoo. Dann packt die asynchrone Methode das Ergebnis einfach neu Task<IFoo>und los.

Andrew Arnott
quelle
1
Stimmen Sie zu, das ist wirklich ärgerlich - ich glaube, die zugrunde liegende Ursache ist, dass sie Task<>unveränderlich ist.
StuartLC
7

Wenn Sie die ansonsten einfache "Thunk" -Methode asynchronisieren, wird eine asynchrone Zustandsmaschine im Speicher erstellt, während die nicht asynchrone dies nicht tut. Während dies oft darauf hindeutet, dass die nicht asynchrone Version verwendet wird, weil sie effizienter ist (was wahr ist), bedeutet dies auch, dass Sie im Falle eines Hangs keine Beweise dafür haben, dass diese Methode am "Rückgabe- / Fortsetzungsstapel" beteiligt ist. was es manchmal schwieriger macht, den Hang zu verstehen.

Also ja, wenn perf nicht kritisch ist (und normalerweise auch nicht), werde ich all diese Thunk-Methoden asynchronisieren, damit ich die asynchrone Zustandsmaschine habe, die mir hilft, Hänge später zu diagnostizieren und um sicherzustellen, dass dies der Fall ist Wenn sich Thunk-Methoden im Laufe der Zeit weiterentwickeln, werden sie sicher fehlerhafte Aufgaben zurückgeben, anstatt sie zu werfen.

Andrew Arnott
quelle
6

Wenn Sie return wait nicht verwenden, können Sie Ihren Stack-Trace beim Debuggen oder beim Drucken in den Protokollen bei Ausnahmen ruinieren.

Wenn Sie die Aufgabe zurückgeben, hat die Methode ihren Zweck erfüllt und befindet sich außerhalb des Aufrufstapels. Wenn Sie verwenden return await, belassen Sie es im Aufrufstapel.

Beispielsweise:

Aufrufstapel bei Verwendung von Warten: A wartet auf die Aufgabe von B => B wartet auf die Aufgabe von C.

Aufrufliste , wenn nicht unter Verwendung Await: A Warten auf die Aufgabe von C, die B zurückgekehrt ist.

haimb
quelle
2

Das verwirrt mich auch und ich habe das Gefühl, dass die vorherigen Antworten Ihre eigentliche Frage übersehen haben:

Warum das Konstrukt return await verwenden, wenn Sie Task direkt vom inneren DoAnotherThingAsync () -Aufruf zurückgeben können?

Nun, manchmal möchten Sie tatsächlich eine Task<SomeType>, aber meistens möchten Sie tatsächlich eine Instanz SomeType, dh das Ergebnis der Aufgabe.

Aus Ihrem Code:

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

Eine Person, die mit der Syntax nicht vertraut ist (ich zum Beispiel), könnte denken, dass diese Methode a zurückgeben sollte Task<SomeResult>, aber da sie mit markiert ist async, bedeutet dies, dass ihr tatsächlicher Rückgabetyp ist SomeResult. Wenn Sie nur verwenden return foo.DoAnotherThingAsync(), geben Sie eine Aufgabe zurück, die nicht kompiliert werden kann. Der richtige Weg ist, das Ergebnis der Aufgabe zurückzugeben, also die return await.

Heltonbiker
quelle
1
"tatsächlicher Rückgabetyp". Eh? async / await ändert die Rückgabetypen nicht. In Ihrem Beispiel var task = DoSomethingAsync();würde Ihnen eine Aufgabe geben, nichtT
Schuh
2
@ Schuh Ich bin nicht sicher, ob ich die async/awaitSache gut verstanden habe. Nach meinem Verständnis Task task = DoSomethingAsync(), während Something something = await DoSomethingAsync()beide arbeiten. Die erste gibt Ihnen die richtige Aufgabe, während die zweite aufgrund des awaitSchlüsselworts das Ergebnis der Aufgabe nach Abschluss liefert . Ich könnte zum Beispiel haben Task task = DoSomethingAsync(); Something something = await task;.
Heltonbiker