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 méthode FindAsync
récupère un objet Department
par son ID et renvoie un 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 synchrone appel.
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?
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 de department
juste après, si je comprends bien les choses), 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. terminé hors machine (et signalé par un événement ou un port d'achèvement).
Si vous avez appelé d.Departments.Find(id)
, le thread reste là 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 CPU lorsque le disque est lié.
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/attente est que je peux facilement continuer à faire d'autres choses pendant que j'attends la fin d'une tâche potentiellement longue. 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 cependant pas nécessaire d’attendre. 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 conduire à des améliorations significatives des performances. Nous avons plusieurs cœurs de nos jours, autant les utiliser.
Il y a donc plus de choses qui se passent dans les 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 thread différent ).
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.
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.
J'aime penser à "async" comme un contrat, un contrat qui dit "je peux l'exécuter de façon 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 plus bas la fonction que vous appelez
Department d = await deptTask;
le traitant donc comme une fonction asynchrone.
C'est à vous.