Pourquoi voudriez-vous jamais «attendre» une méthode, puis interroger immédiatement sa valeur de retour?

24

Dans cet article MSDN , l'exemple de code suivant est fourni (légèrement modifié par souci de concision):

public async Task<ActionResult> Details(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }

    Department department = await db.Departments.FindAsync(id);

    if (department == null)
    {
        return HttpNotFound();
    }

    return View(department);
}

La FindAsyncméthode récupère un Departmentobjet par son ID et renvoie a Task<Department>. Ensuite, le département est immédiatement vérifié pour voir s'il est nul. Si je comprends bien, demander la valeur de la tâche de cette manière bloquera l'exécution du code jusqu'à ce que la valeur de la méthode attendue soit renvoyée, ce qui en fait un appel synchrone.

Pourquoi voudriez-vous faire ça? Ne serait-il pas plus simple d'appeler simplement la méthode synchrone Find(id), si vous voulez bloquer immédiatement de toute façon?

Robert Harvey
la source
Cela pourrait être lié à la mise en œuvre. ... else return null;Ensuite, vous devez vérifier que la méthode a effectivement trouvé le service que vous avez demandé.
Jeremy Kato
Je n'en vois pas dans un asp.net, mais dans une application de destop, en procédant de cette façon, vous ne gelez pas l'interface utilisateur
Rémi
Voici un lien qui explique le concept d'attente du concepteur ... msdn.microsoft.com/en-us/magazine/hh456401.aspx
Jon Raynor
Attendre ne vaut la peine de penser à ASP.NET que si les commutateurs de contact de threads vous ralentissent ou si l'utilisation de la mémoire de nombreuses piles de threads est un problème pour vous.
Ian

Réponses:

24

Si je comprends bien, demander la valeur de la tâche de cette manière bloquera l'exécution du code jusqu'à ce que la valeur de la méthode attendue soit renvoyée, ce qui en fait un appel synchrone.

Pas assez.

Lorsque vous appelez, await db.Departments.FindAsync(id)la tâche est envoyée et le thread actuel est renvoyé au pool pour être utilisé par d'autres opérations. Le flux d'exécution est bloqué (comme il le serait indépendamment de l'utilisation departmentjuste après, si je comprends bien), mais le thread lui-même est libre d'être utilisé par d'autres choses pendant que vous attendez que l'opération soit terminée hors machine (et signalée par un événement ou un port d'achèvement).

Si vous avez appelé, d.Departments.Find(id)le thread reste assis et attend la réponse, même si la plupart du traitement est effectué sur la base de données.

Vous libérez efficacement les ressources du processeur lorsque le disque est lié.

Telastyn
la source
2
Mais je pensais que tout ce awaitqui était fait était de signer sur le reste de la méthode en tant que continuation sur le même thread (il y a des exceptions; certaines méthodes asynchrones font tourner leur propre thread), ou de signer la asyncméthode en tant que continuation sur le même thread et autoriser la code restant à exécuter (comme vous pouvez le voir, je ne suis pas clair sur le asyncfonctionnement). Ce que vous décrivez ressemble plus à une forme sophistiquée deThread.Sleep(untilReturnValueAvailable)
Robert Harvey
1
@RobertHarvey - il l'affecte en tant que continuation, mais une fois que vous avez envoyé la tâche (et sa continuation) pour qu'elle soit traitée, il ne reste plus rien à exécuter. Il n'est pas garanti d'être sur le même thread sauf si vous le spécifiez (via ConfigureAwaitiirc).
Telastyn
1
Voir ma réponse ... la suite est reclassée sur le fil d'origine par défaut.
Michael Brown
2
Je pense que je vois ce qui me manque ici. Pour que cela fournisse des avantages, le framework ASP.NET MVC doit awaitson appel à public async Task<ActionResult> Details(int? id). Sinon, l'appel d'origine sera simplement bloqué, en attente department == nullde résolution.
Robert Harvey
2
@RobertHarvey Au moment du await ..."retour", l' FindAsyncappel est terminé. C'est ce qui vous attend. Cela s'appelle attendre car cela fait attendre votre code. (Mais notez que ce n'est pas la même chose que de faire attendre le thread actuel)
user253751
17

Je déteste vraiment qu'aucun des exemples ne montre comment il est possible d'attendre quelques lignes avant d'attendre la tâche. Considère ceci.

Foo foo = await getFoo();
Bar bar = await getBar();

Console.WriteLine(“Do some other stuff to prepare.”);

doStuff(foo, bar);

C'est le genre de code que les exemples encouragent, et vous avez raison. Cela a peu de sens. Cela libère le thread principal pour faire d'autres choses, comme répondre à l'entrée de l'interface utilisateur, mais la vraie puissance de l'async / wait est que je peux facilement continuer à faire d'autres choses pendant que j'attends qu'une tâche potentiellement longue soit terminée. Le code ci-dessus "bloquera" et attendra pour exécuter la ligne d'impression jusqu'à ce que nous ayons obtenu Foo & Bar. Il n'est pas nécessaire d'attendre cependant. Nous pouvons traiter cela en attendant.

Task<Foo> foo = getFoo();
Task<Bar> bar = getBar();

Console.WriteLine(“Do some other stuff to prepare.”);

doStuff(await foo, await bar);

Maintenant, avec le code réécrit, nous ne nous arrêtons pas et n'attendons pas nos valeurs jusqu'à ce que nous le devions. Je suis toujours à la recherche de ce genre d'opportunités. Être intelligent quand nous attendons peut entraîner des améliorations significatives des performances. Nous avons plusieurs cœurs de nos jours, autant les utiliser.

Canard en caoutchouc
la source
1
Hm, cela concerne moins les cœurs / threads et plus l'utilisation correcte des appels asynchrones.
Déduplicateur
Vous n'avez pas tort @Deduplicator. C'est pourquoi j'ai cité «bloc» dans ma réponse. Il est difficile de parler de ces choses sans mentionner les threads, mais tout en reconnaissant qu'il peut y avoir ou non du multi-threading.
RubberDuck
Je vous suggère de faire attention à attendre dans un autre appel de méthode. Parfois, le compilateur comprend mal cette attente et vous obtenez une exception vraiment bizarre. Après tout async / wait n'est que du sucre de syntaxe, vous pouvez vérifier avec sharplab.io à quoi ressemble le code généré. J'ai observé cela à plusieurs reprises et maintenant j'attends juste une ligne au-dessus de l'appel où le résultat est nécessaire ... pas besoin de ces maux de tête.
evictednoise
"Il y a peu de sens là-dedans." - Ça a beaucoup de sens. Les cas où "continuer à faire autre chose" est applicable dans la même méthode ne sont pas si courants; il est beaucoup plus courant que vous vouliez simplement awaitlaisser le fil faire des choses complètement différentes à la place.
Sebastian Redl
Quand j'ai dit «cela n'a pas de sens» @SebastianRedl, je voulais dire le cas où vous pourriez évidemment exécuter les deux tâches en parallèle plutôt que d'exécuter, d'attendre, d'exécuter, d'attendre. C'est beaucoup plus courant que vous ne le pensez. Je parie que si vous regardez autour de votre base de code, vous trouverez des opportunités.
RubberDuck
6

Il y a donc plus de choses qui se passent en coulisses ici. Async / Await est du sucre syntaxique. Regardez d'abord la signature de la fonction FindAsync. Il renvoie une tâche. Vous voyez déjà la magie du mot-clé, il déballe cette tâche dans un département.

La fonction appelante ne bloque pas. Ce qui se passe, c'est que l'affectation au département et tout ce qui suit le mot-clé wait est encadrée dans une fermeture et à toutes fins utiles passée à la méthode Task.ContinueWith (la fonction FindAsync est automatiquement exécutée sur un autre thread).

Bien sûr, il se passe plus de choses en arrière-plan, car l'opération est redirigée vers le thread d'origine (vous n'avez donc plus à vous soucier de la synchronisation avec l'interface utilisateur lors de l'exécution d'une opération en arrière-plan) et dans le cas où la fonction appelante est Async ( et étant appelé de manière asynchrone), la même chose se produit dans la pile.

Donc, ce qui se passe, c'est que vous obtenez la magie des opérations Async, sans les pièges.

Michael Brown
la source
1

Non, ça ne revient pas immédiatement. L'attente rend l'appel de méthode asynchrone. Lorsque FindAsync est appelé, la méthode Details retourne avec une tâche qui n'est pas terminée. Une fois FindAsync terminé, il renvoie son résultat dans la variable department et reprend le reste de la méthode Details.

Steve
la source
Non, il n'y a aucun blocage. Il n'atteindra jamais department == null tant que la méthode async n'est pas terminée.
Steve
1
async awaitne crée généralement pas de nouveaux threads, et même si c'est le cas, vous devez toujours attendre la valeur de ce département pour savoir si elle est nulle.
Robert Harvey
2
Donc, fondamentalement, ce que vous dites, c'est que, pour que cela profite du tout, l'appel à public async Task<ActionResult> doit également êtreawait
Robert Harvey
2
@RobertHarvey Oui, vous avez l'idée. Async / Await est essentiellement viral. Si vous "attendez" une fonction, vous devez également attendre toutes les fonctions qui l'appellent. awaitne doit pas être mélangé avec .Wait()ou .Result, car cela peut provoquer des blocages. La chaîne asynchrone / attente se termine généralement par une fonction avec une async voidsignature, qui est principalement utilisée pour les gestionnaires d'événements ou pour les fonctions appelées directement par les éléments d'interface utilisateur.
KChaloux
1
@Steve - " ... donc ce serait sur son propre thread. " Non, les tâches de lecture ne sont toujours pas des threads et async n'est pas parallèle . Cela inclut la sortie réelle démontrant qu'un nouveau thread n'est pas créé.
ToolmakerSteve
1

J'aime à penser à "async" comme un contrat, un contrat qui dit "je peux l'exécuter de manière asynchrone si vous en avez besoin, mais vous pouvez aussi m'appeler comme n'importe quelle autre fonction synchrone".

Autrement dit, un développeur faisait des fonctions et certaines décisions de conception les ont amenés à créer / marquer un tas de fonctions comme "asynchrones". L'appelant / consommateur des fonctions est libre de les utiliser à sa guise. Comme vous le dites, vous pouvez soit appeler wait juste avant l'appel de fonction et l'attendre, de cette façon vous l'avez traité comme une fonction synchrone, mais si vous le souhaitez, vous pouvez l'appeler sans attendre car

Task<Department> deptTask = db.Departments.FindAsync(id);

et après, disons, 10 lignes vers le bas de la fonction que vous appelez

Department d = await deptTask;

le traitant donc comme une fonction asynchrone.

C'est à vous.

user734028
la source
1
C'est plus comme "Je me réserve le droit de gérer le message de manière asynchrone, utilisez-le pour obtenir le résultat".
Déduplicateur
-1

"si vous voulez bloquer immédiatement", la réponse est "Oui". Uniquement lorsque vous avez besoin d'une réponse rapide, attendre / async est logique. Par exemple, le thread d'interface utilisateur arrive à une méthode vraiment asynchrone, le thread d'interface utilisateur retournera et continuera à écouter le clic du bouton, tandis que le code ci-dessous "attendre" sera excuté par un autre thread et obtiendra finalement le résultat.

user10200952
la source
1
Qu'est-ce que cette réponse apporte que les autres réponses ne font pas?