Quel est le but du «retour en attente» en C #?

251

Y a-t-il un scénario où une méthode d'écriture comme celle-ci:

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

au lieu de cela:

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

aurait du sens?

Pourquoi utiliser la return awaitconstruction alors que vous pouvez directement revenir Task<T>de l' DoAnotherThingAsync()invocation intérieure ?

Je vois du code avec return awaittant d'endroits, je pense que j'ai peut-être raté quelque chose. Mais pour autant que je sache, ne pas utiliser de mots clés async / wait dans ce cas et renvoyer directement la tâche serait fonctionnellement équivalent. Pourquoi ajouter des frais généraux supplémentaires de awaitcouche supplémentaire ?

TX_
la source
2
Je pense que la seule raison pour laquelle vous voyez cela est que les gens apprennent par imitation et généralement (s'ils n'en ont pas besoin) ils utilisent la solution la plus simple qu'ils peuvent trouver. Donc, les gens voient ce code, utilisent ce code, ils voient que cela fonctionne et à partir de maintenant, pour eux, c'est la bonne façon de le faire ... Inutile d'attendre dans ce cas
Fabio Marcolini
7
Il y a au moins une différence importante: la propagation des exceptions .
noseratio
1
Je ne le comprends pas non plus, je ne comprends pas du tout ce concept, cela n'a aucun sens. D'après ce que j'ai appris si une méthode a un type de retour, IL DOIT avoir un mot-clé de retour, n'est-ce pas les règles du langage C #?
monstro
@monstro la question de l'OP a bien la déclaration de retour?
David Klempfner

Réponses:

189

Il y a un cas sournois quand returndans la méthode normale et return awaitdans la asyncméthode se comportent différemment: lorsqu'ils sont combinés avec using(ou, plus généralement, tout return awaitdans un trybloc).

Considérez ces deux versions d'une méthode:

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

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

La première méthode sera Dispose()l' Fooobjet dès le DoAnotherThingAsync()retour de la méthode, ce qui est probablement bien avant qu'elle ne se termine réellement. Cela signifie que la première version est probablement boguée (car Foosupprimée trop tôt), tandis que la deuxième version fonctionnera correctement.

svick
la source
4
Pour être complet, dans le premier cas, vous devez revenirfoo.DoAnotherThingAsync().ContinueWith(_ => foo.Dispose());
ghord
7
@ghord Cela ne fonctionnerait pas, Dispose()revient void. Vous auriez besoin de quelque chose comme return foo.DoAnotherThingAsync().ContinueWith(t -> { foo.Dispose(); return t.Result; });. Mais je ne sais pas pourquoi feriez-vous cela lorsque vous pouvez utiliser la deuxième option.
svick
1
@svick Vous avez raison, cela devrait être plus dans le sens de { var task = DoAnotherThingAsync(); task.ContinueWith(_ => foo.Dispose()); return task; }. Le cas d'utilisation est assez simple: si vous êtes sur .NET 4.0 (comme la plupart), vous pouvez toujours écrire du code asynchrone de cette façon qui fonctionnera bien à partir des applications 4.5.
ghord
2
@ghord Si vous êtes sur .Net 4.0 et que vous souhaitez écrire du code asynchrone, vous devez probablement utiliser Microsoft.Bcl.Async . Et votre code ne dispose Fooqu'après la fin du retour Task, ce que je n'aime pas, car il introduit inutilement la concurrence.
svick
1
@svick Votre code attend également que la tâche soit terminée. En outre, Microsoft.Bcl.Async est inutilisable pour moi en raison de la dépendance à KB2468871 et des conflits lors de l'utilisation de la base de code asynchrone .NET 4.0 avec le code asynchrone 4.5 approprié.
ghord
93

Si vous n'en avez pas besoin async(c.-à-d., Vous pouvez retourner le Taskdirectement), alors ne l'utilisez pas async.

Il y a certaines situations où cela return awaitest utile, comme si vous avez deux opérations asynchrones à faire:

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

Pour plus d'informations sur les asyncperformances, consultez l' article et la vidéo MSDN de Stephen Toub sur le sujet.

Mise à jour: j'ai écrit un article de blog qui va beaucoup plus en détail.

Stephen Cleary
la source
13
Pourriez-vous ajouter une explication sur la raison pour laquelle le awaitest utile dans le deuxième cas? Pourquoi pas return SecondAwait(intermediate);?
Matt Smith
2
J'ai la même question que Matt, n'atteindrait-il pas aussi return SecondAwait(intermediate);l'objectif dans ce cas? Je pense que return awaitc'est redondant ici aussi ...
TX_
23
@MattSmith Cela ne compilerait pas. Si vous souhaitez utiliser awaitdans la première ligne, vous devez également l'utiliser dans la seconde.
svick
2
@cateyes Je ne sais pas ce que signifie «surcharge introduite en les mettant en parallèle», mais la asyncversion utilisera moins de ressources (threads) que votre version synchrone.
svick
4
@TomLint Il ne compile vraiment pas. En supposant que le type de retour SecondAwaitest `chaîne, le message d'erreur est:" CS4016: Puisqu'il s'agit d'une méthode asynchrone, l'expression de retour doit être de type 'chaîne' plutôt que 'Tâche <chaîne>' ".
svick
23

La seule raison pour laquelle vous voudriez le faire est s'il y en a un autre awaitdans le code précédent, ou si vous manipulez le résultat d'une manière ou d'une autre avant de le retourner. Une autre façon dont cela pourrait se produire est de try/catchmodifier la façon dont les exceptions sont gérées. Si vous ne faites rien de tout cela, vous avez raison, il n'y a aucune raison d'ajouter la surcharge de la méthode async.

Servy
la source
4
Comme pour la réponse de Stephen, je ne comprends pas pourquoi serait return awaitnécessaire (au lieu de simplement renvoyer la tâche d'invocation d'enfant) même s'il y en a d'autres en attente dans le code précédent . Pourriez-vous s'il vous plaît fournir des explications?
TX_
10
@TX_ Si vous souhaitez supprimer, asynccomment attendriez-vous la première tâche? Vous devez marquer la méthode comme asyncsi vous souhaitez utiliser des attentes. Si la méthode est marquée comme asyncet que vous avez un awaitcode antérieur, vous devez effectuer awaitla deuxième opération asynchrone pour qu'elle soit du type approprié. Si vous venez de le supprimer, awaitil ne se compilera pas car la valeur de retour ne sera pas du type approprié. Puisque la méthode est asyncle résultat est toujours enveloppé dans une tâche.
Servy
11
@Noseratio Essayez les deux. Le premier se compile. La seconde non. Le message d'erreur vous indiquera le problème. Vous ne retournerez pas le type approprié. Lorsqu'une asyncméthode ne renvoie pas de tâche, vous retournez le résultat de la tâche qui sera ensuite encapsulé.
Servy
3
@Servy, bien sûr - vous avez raison. Dans ce dernier cas, nous retournerions Task<Type>explicitement, tandis que asyncdicte de retourner Type(ce que le compilateur lui-même transformerait Task<Type>).
noseratio
3
@Itsik Bien sûr, asyncc'est juste du sucre syntaxique pour câbler explicitement les suites. Vous ne devez async rien faire, mais quand vous livrez à une opération asynchrone non triviale , il est considérablement plus facile de travailler avec. Par exemple, le code que vous avez fourni ne propage pas réellement les erreurs comme vous le souhaitez, et le faire correctement dans des situations encore plus complexes commence à devenir beaucoup plus difficile. Bien que vous n'en ayez jamais besoin async , les situations que je décris sont celles où il ajoute de la valeur pour l'utiliser.
Servy
17

Un autre cas dont vous devrez peut-être attendre le résultat est celui-ci:

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

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

Dans ce cas, GetIFooAsync()doit attendre le résultat GetFooAsynccar le type de Test différent entre les deux méthodes et Task<Foo>n'est pas directement attribuable à Task<IFoo>. Mais si vous attendez le résultat, il devient tout simplement ce Fooqui est directement attribuable à IFoo. Ensuite, la méthode async reconditionne simplement le résultat à l'intérieur Task<IFoo>et à l'extérieur.

Andrew Arnott
la source
1
D'accord, c'est vraiment ennuyeux - je crois que la cause sous-jacente Task<>est invariante.
StuartLC
7

Rendre la méthode "thunk" autrement simple asynchrone crée une machine d'état asynchrone en mémoire alors que celle non-async ne le fait pas. Bien que cela puisse souvent inciter les gens à utiliser la version non asynchrone car elle est plus efficace (ce qui est vrai), cela signifie également qu'en cas de blocage, vous n'avez aucune preuve que cette méthode est impliquée dans la "pile de retour / continuation". ce qui rend parfois plus difficile la compréhension du blocage.

Alors oui, lorsque la perf n'est pas critique (et ce n'est généralement pas le cas), je lancerai asynchrone sur toutes ces méthodes de thunk afin d'avoir la machine d'état asynchrone pour m'aider à diagnostiquer les blocages plus tard, et aussi pour m'assurer que si ces les méthodes de thunk évoluent au fil du temps, elles seront sûres de retourner les tâches en défaut au lieu de lancer.

Andrew Arnott
la source
6

Si vous n'utilisez pas return wait, vous pouvez ruiner la trace de votre pile pendant le débogage ou lorsqu'elle est imprimée dans les journaux des exceptions.

Lorsque vous renvoyez la tâche, la méthode a rempli son objectif et elle est hors de la pile d'appels. Lorsque vous utilisez, return awaitvous le laissez dans la pile des appels.

Par exemple:

Pile d'appels lors de l'utilisation de l'attente: A en attente de la tâche de B => B en attente de la tâche de C

Pile d'appels lorsque vous n'utilisez pas wait: A attend la tâche de C, que B a renvoyée.

haimb
la source
2

Cela m'embrouille également et je pense que les réponses précédentes ont ignoré votre question réelle:

Pourquoi utiliser la construction return wait alors que vous pouvez directement renvoyer Task à partir de l'invocation intérieure DoAnotherThingAsync ()?

Eh bien, parfois, vous voulez vraiment un Task<SomeType>, mais la plupart du temps, vous voulez en fait une instance SomeType, c'est-à-dire le résultat de la tâche.

Depuis votre code:

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

Une personne qui ne connaît pas la syntaxe (moi, par exemple) pourrait penser que cette méthode devrait retourner un Task<SomeResult>, mais comme elle est marquée avec async, cela signifie que son type de retour réel est SomeResult. Si vous utilisez simplement return foo.DoAnotherThingAsync(), vous retourneriez une tâche qui ne serait pas compilée. La bonne façon est de renvoyer le résultat de la tâche, donc le return await.

heltonbiker
la source
1
"type de retour réel". Eh? async / wait ne change pas les types de retour. Dans votre exemple, var task = DoSomethingAsync();cela vous donnerait une tâche, pasT
Chaussure
2
@Shoe Je ne suis pas sûr d'avoir bien compris la async/awaitchose. À ma connaissance Task task = DoSomethingAsync(), alors que les Something something = await DoSomethingAsync()deux fonctionnent. Le premier vous donne la tâche appropriée, tandis que le second, en raison du awaitmot - clé, vous donne le résultat de la tâche une fois terminée. Je pourrais, par exemple, avoir Task task = DoSomethingAsync(); Something something = await task;.
heltonbiker