En fait, async/await n'est pas si magique. Le sujet est assez vaste, mais pour une réponse rapide mais suffisamment complète à votre question, je pense que nous pouvons nous débrouiller.
Abordons un simple événement de clic de bouton dans une application Windows Forms :
public async void button1_Click(object sender, EventArgs e)
{
Console.WriteLine("before awaiting");
await GetSomethingAsync();
Console.WriteLine("after awaiting");
}
Je vais explicitement pas parler de tout ce qui est GetSomethingAsync
est de retour pour le moment. Disons que c'est quelque chose qui se terminera après, disons, 2 secondes.
Dans un monde traditionnel, non asynchrone, votre gestionnaire d'événement de clic de bouton ressemblerait à quelque chose comme ceci :
public void button1_Click(object sender, EventArgs e)
{
Console.WriteLine("before waiting");
DoSomethingThatTakes2Seconds();
Console.WriteLine("after waiting");
}
Lorsque vous cliquez sur le bouton du formulaire, l'application semble se figer pendant environ 2 secondes, le temps que la méthode se termine. Ce qui se passe, c'est que la "pompe à messages", qui est en fait une boucle, est bloquée.
Cette boucle demande continuellement à Windows "Quelqu'un a-t-il fait quelque chose, comme déplacer la souris, cliquer sur quelque chose ? Est-ce que je dois repeindre quelque chose ? Si oui, dites-le moi", puis traite ce "quelque chose". Cette boucle a reçu un message indiquant que l'utilisateur a cliqué sur le "bouton1" (ou le type de message équivalent de Windows), et a fini par appeler notre module button1_Click
méthode ci-dessus. Jusqu'à ce que cette méthode revienne, cette boucle est maintenant bloquée en attente. Cela prend 2 secondes et pendant ce temps, aucun message n'est traité.
La plupart des choses qui concernent Windows se font à l'aide de messages, ce qui signifie que si la boucle de messages cesse de pomper des messages, ne serait-ce qu'une seconde, l'utilisateur le remarque rapidement. Par exemple, si vous déplacez le bloc-notes ou tout autre programme au-dessus de votre propre programme, puis l'en éloignez à nouveau, une rafale de messages de peinture est envoyée à votre programme pour indiquer quelle région de la fenêtre est soudainement redevenue visible. Si la boucle de messages qui traite ces messages est en attente de quelque chose, bloquée, alors aucune peinture n'est effectuée.
Donc, si dans le premier exemple, async/await
ne crée pas de nouveaux fils, comment fait-il ?
Eh bien, ce qui se passe, c'est que votre méthode est divisée en deux. Comme il s'agit d'un sujet très vaste, je ne vais pas entrer dans les détails, mais il suffit de dire que la méthode est divisée en deux :
- Tout le code menant à
await
y compris l'appel à GetSomethingAsync
- Tout le code suivant
await
Illustration :
code... code... code... await X(); ... code... code... code...
Réarrangé :
code... code... code... var x = X(); await X; code... code... code...
^ ^ ^ ^
+---- portion 1 -------------------+ +---- portion 2 ------+
En gros, la méthode s'exécute comme suit :
-
Il exécute tout jusqu'à await
-
Il appelle le GetSomethingAsync
qui fait son travail et renvoie quelque chose qui se terminera 2 secondes dans le futur
Jusqu'à présent, nous sommes toujours dans l'appel original à button1_Click, qui se passe sur le fil principal, appelé depuis la boucle de messages. Si le code menant à await
prend beaucoup de temps, l'interface utilisateur se figera quand même. Dans notre exemple, pas tant que ça
-
Qu'est-ce que le await
ainsi qu'une astuce de compilation, fait qu'il fait quelque chose comme "Ok, vous savez quoi, je vais simplement retourner du gestionnaire d'événement de clic de bouton ici. Lorsque vous (c'est-à-dire la chose que nous attendons) aurez terminé, faites-le moi savoir car il me reste encore du code à exécuter".
En fait, cela permettra au Classe SynchronizationContext sait qu'elle est terminée, ce qui, en fonction du contexte de synchronisation en cours, fera la queue pour l'exécution. La classe de contexte utilisée dans un programme Windows Forms la mettra en file d'attente en utilisant la file d'attente que la boucle de messages pompe.
-
Il retourne donc à la boucle de messages, qui est maintenant libre de continuer à pomper des messages, comme déplacer la fenêtre, la redimensionner ou cliquer sur d'autres boutons.
Pour l'utilisateur, l'interface utilisateur est à nouveau réactive, elle traite les autres clics de bouton, les redimensionnements et, surtout, le plus important, redessiner pour qu'il ne semble pas geler.
-
2 secondes plus tard, la chose que nous attendons se termine et ce qui se passe maintenant, c'est qu'il (enfin, le contexte de synchronisation) place un message dans la file d'attente que la boucle de message regarde, en disant "Hé, j'ai encore du code à exécuter", et ce code est tout le code après l'attendre.
-
Lorsque la boucle de messages arrive à ce message, elle "réintègre" la méthode là où elle s'est arrêtée, juste après le message suivant await
et continuer à exécuter le reste de la méthode. Notez que ce code est à nouveau appelé depuis la boucle de messages, donc si ce code fait quelque chose de long sans utiliser la méthode async/await
correctement, il bloquera à nouveau la boucle de messages
Il y a beaucoup de pièces mobiles sous le capot ici, alors voici quelques liens vers plus d'informations, j'allais dire "si vous en avez besoin", mais ce sujet est assez large et il est assez important de savoir certaines de ces pièces mobiles . Invariablement, vous comprendrez qu'async/await reste un concept peu fiable. Certaines des limitations et des problèmes sous-jacents s'infiltrent toujours dans le code environnant, et si ce n'est pas le cas, vous finissez généralement par devoir déboguer une application qui s'arrête de manière aléatoire sans raison valable.
OK, et si GetSomethingAsync
lance un fil de discussion qui se termine en 2 secondes ? Oui, alors il est évident qu'il y a un nouveau fil en jeu. Ce fil, cependant, n'est pas parce que de l'asynchronisme de cette méthode, c'est parce que le programmeur de cette méthode a choisi un thread pour implémenter le code asynchrone. Presque toutes les E/S asynchrones Ne le fais pas. utilisent un fil, ils utilisent des choses différentes. async/await
par eux-mêmes ne créent pas de nouveaux threads, mais il est évident que les "choses que nous attendons" peuvent être mises en œuvre à l'aide de threads.
Il y a beaucoup de choses dans .NET qui ne font pas nécessairement tourner un thread par elles-mêmes mais qui sont quand même asynchrones :
- Requêtes web (et bien d'autres choses liées au réseau qui prennent du temps)
- Lecture et écriture asynchrones de fichiers
- et bien d'autres encore, un bon signe est que la classe/interface en question possède des méthodes nommées
SomethingSomethingAsync
o BeginSomething
y EndSomething
et il y a un IAsyncResult
impliqué.
Habituellement, ces choses n'utilisent pas de fil sous le capot.
OK, donc tu veux un peu de ces "trucs à large sujet" ?
Eh bien, demandons Essayez Roslyn sur notre bouton clic :
Essayez Roslyn
Je ne vais pas mettre le lien vers le cours complet généré ici, mais c'est assez gore.
22 votes
Les tâches IO ne sont pas liées au CPU et ne nécessitent donc pas de thread. L'objectif principal de l'asynchronisme est de ne pas bloquer les threads pendant les tâches liées à l'IO.
0 votes
Je suis d'accord avec vous. Un nouveau processus doit être lancé.
27 votes
@jdweng : Non, pas du tout. Même si cela créait de nouvelles fils c'est très différent de la création d'un nouveau processus.
5 votes
Supposons que vous fassiez une demande sur le Web. Entre l'envoi de la demande et la réception de la réponse, considérez-vous que vous "faites" quelque chose ? Votre programme n'a pas besoin d'exécuter de code, et pourtant quelque chose d'utile se produit.
10 votes
Si vous comprenez la programmation asynchrone basée sur le callback, alors vous comprenez comment
await
/async
fonctionne sans créer de fils.7 votes
Ce n'est pas exactement faire une application plus réactive, mais il vous décourage de bloquer vos threads, ce qui est une cause fréquente des applications non réactives.
0 votes
@JonSkeet corrigez-moi si je me trompe, mais n'ai-je pas lu dans votre livre que async-await peut ou ne peut pas utiliser un thread du pool ? J'ai du mal à me rappeler où je l'ai lu.
11 votes
@RubberDuck : Oui, il peut utiliser un thread du pool de threads pour la continuation. Mais il ne s'agit pas de démarrer un thread de la manière dont l'OP l'imagine ici - ce n'est pas comme s'il disait "Prenez cette méthode ordinaire, maintenant exécutez-la dans un thread séparé - voilà, c'est asynchrone". C'est beaucoup plus subtil que cela.
0 votes
C'est vrai. C'est ce que je pensais. Merci @JonSkeet
2 votes
Parfois, une tâche a simplement besoin d'attendre, et ce n'est pas un problème de threading. Disons que la tâche A attend l'appel A pendant 300 millisecondes. La tâche B arrive et a besoin d'exécuter le Local B pendant 3 nanosecondes. La tâche B doit-elle attendre l'appel A ? J'espère que vous avez répondu "non". Attendez le retour de l'appel A et permettez à Local B de se faufiler.