Si async-waiting ne crée pas de threads supplémentaires, alors comment rendre les applications réactives?

À maintes resockets, je vois qu’il a déclaré que l’utilisation d’ asyncawait ne crée pas de threads supplémentaires. Cela n’a pas de sens, car les seuls moyens par lesquels un ordinateur peut sembler faire plus d’une chose à la fois est

  • Effectuer plus d’une chose à la fois (en parallèle, en utilisant plusieurs processeurs)
  • Le simuler en planifiant des tâches et en les basculant (faites un peu de A, un peu de B, un peu de A, etc.)

Donc, si asyncawait ne fait ni l’un ni l’autre, alors comment peut-il rendre une application réactive? S’il n’y a qu’un seul thread, l’appel de n’importe quelle méthode signifie qu’il faut attendre que la méthode se termine avant de faire quoi que ce soit, et les méthodes de cette méthode doivent attendre le résultat avant de continuer, et ainsi de suite.

En fait, async / wait n’est pas si magique. Le sujet complet est assez large mais pour une réponse rapide mais suffisamment complète à votre question, je pense que nous pouvons gérer.

Abordons un simple événement de clic sur un 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 ne pas parler de ce que GetSomethingAsync renvoie pour le moment. Disons simplement que c’est quelque chose qui se terminera après, disons, 2 secondes.

Dans un monde traditionnel, non asynchrone, votre gestionnaire d’événements de clic sur un bouton ressemble à ceci:

 public void button1_Click(object sender, EventArgs e) { Console.WriteLine("before waiting"); DoSomethingThatTakes2Seconds(); Console.WriteLine("after waiting"); } 

Lorsque vous cliquez sur le bouton dans le formulaire, l’application semble se figer pendant environ 2 secondes, en attendant que cette méthode se termine. Ce qui se passe est que la “pompe à messages”, essentiellement une boucle, est bloquée.

Cette boucle demande continuellement à Windows “Quelqu’un at-il fait quelque chose, comme déplacé la souris, cliqué sur quelque chose? Dois-je repeindre quelque chose? Si oui, dis-le moi!” et traite ensuite ce “quelque chose”. Cette boucle a reçu un message indiquant que l’utilisateur a cliqué sur “button1” (ou le type de message équivalent de Windows) et a finalement appelé notre méthode button1_Click ci-dessus. Tant que cette méthode ne revient pas, cette boucle est maintenant bloquée en attente. Cela prend 2 secondes et pendant ce temps, aucun message n’est en cours de traitement.

La plupart des choses qui traitent des fenêtres se font à l’aide de messages, ce qui signifie que si la boucle de messages arrête de pomper des messages, même pour une seconde seulement, elle est rapidement perceptible par l’utilisateur. Par exemple, si vous déplacez le bloc-notes ou tout autre programme au-dessus de votre propre programme, puis de nouveau, un flot de messages de peinture est envoyé à votre programme pour indiquer la région de la fenêtre qui est soudainement redevenue visible. Si la boucle de messages qui traite ces messages attend quelque chose, bloquée, aucune peinture n’est effectuée.

Donc, si dans le premier exemple, async/await ne crée pas de nouveaux threads, comment le fait-il?

Eh bien, ce qui se passe est que votre méthode est divisée en deux. C’est l’un de ces types de sujet que je ne détaillerai pas, mais il suffit de dire que la méthode est divisée en deux choses:

  1. Tout le code en await , y compris l’appel à GetSomethingAsync
  2. 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 ------+ 

Fondamentalement, la méthode s’exécute comme ceci:

  1. Il exécute tout pour await
  2. Il appelle la méthode GetSomethingAsync , qui fait son travail, et renvoie quelque chose qui se terminera dans 2 secondes à l’avenir

    Jusqu’à présent, nous sums toujours dans l’appel initial à button1_Click, qui se produit sur le thread principal, appelé depuis la boucle de messages. Si le code à await prend beaucoup de temps, l’interface utilisateur se fige toujours. Dans notre exemple, pas tellement

  3. Ce que le mot-clé await , associé à une magie intelligente du compilateur, fait, c’est quelque chose comme “Ok, vous savez quoi, je vais simplement revenir ici du gestionnaire d’événements de bouton-clic. attendez-vous) pour finir, faites le moi savoir car il me rest encore du code à exécuter “.

    En fait, cela permettra à la classe SynchronizationContext de savoir que cela est fait, ce qui, en fonction du contexte de synchronisation réel en jeu, sera mis en queue pour exécution. La classe de contexte utilisée dans un programme Windows Forms le mettra en queue en utilisant la queue pompée par la boucle de messages.

  4. Il revient 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, en traitant les autres clics de bouton, en les redimensionnant et, surtout, en les redessinant , afin qu’ils ne semblent pas se bloquer.

  5. 2 secondes plus tard, la chose que nous attendons est terminée et ce qui se passe maintenant, c’est que le contexte de synchronisation place un message dans la file d’attente de la boucle des messages, en disant: vous exécutez “, et ce code est tout le code après l’attente.
  6. Lorsque la boucle de message parvient à ce message, elle ré-entrera “fondamentalement” cette méthode là où elle l’avait laissée, juste après l’ await et continue d’exécuter le rest de la méthode. Notez que ce code est à nouveau appelé à partir de la boucle de message, donc si ce code arrive à faire quelque chose de long sans utiliser async/await correctement, il bloquera à nouveau la boucle de message

Il y a beaucoup de pièces mobiles sous le capot ici, 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 connaître certaines de ces pièces mobiles . Invariablement, vous allez comprendre que l’async / wait est toujours un concept qui fuit. Certaines des limitations et des problèmes sous-jacents continuent à apparaître dans le code environnant, et si ce n’est pas le cas, vous devez généralement déboguer une application qui se casse de manière aléatoire pour une raison apparemment inexistante.

  • Programmation asynchrone avec Async et Await (C # et Visual Basic)
  • Classe SynchronizationContext
  • Stephen Cleary – Il n’y a pas de fil de discussion qui vaut la peine d’être lu!
  • Channel 9 – Mads Torgersen: Inside C # Async vaut bien une montre!

OK, alors si GetSomethingAsync tourner un thread qui se terminera en 2 secondes? Oui, il y a évidemment un nouveau fil conducteur. Ce thread, cependant, n’est pas à cause de l’async de cette méthode, c’est parce que le programmeur de cette méthode a choisi un thread pour implémenter du code asynchrone. Presque toutes les E / S asynchrones n’utilisent pas de thread, elles utilisent des choses différentes. async/await par euxmêmes ne font pas tourner de nouveaux threads mais les “choses que nous attendons” peuvent évidemment être implémentées en utilisant des threads.

Il y a beaucoup de choses dans .NET qui ne font pas nécessairement tourner un thread par lui-même mais sont toujours asynchrones:

  • Requêtes Web (et beaucoup d’autres choses liées au réseau qui prennent du temps)
  • Lecture et écriture de fichiers asynchrones
  • et bien d’autres encore, un bon signe est que la classe / interface en question a des méthodes nommées SomethingSomethingAsync ou BeginSomething et EndSomething et qu’il y a un IAsyncResult impliqué.

Habituellement, ces choses n’utilisent pas de fil sous le capot.


OK, alors vous voulez une partie de ce “sujet général”?

Eh bien, demandons Essayez Roslyn à propos de notre clic sur le bouton:

Essayez Roslyn

Je ne vais pas créer de lien dans la classe générée ici, mais c’est plutôt génial.

les seuls moyens par lesquels un ordinateur peut sembler faire plus d’une chose à la fois (1) en fait faire plus d’une chose à la fois, (2) la simuler en planifiant des tâches et en les basculant. Donc, si async-wait ne fait ni l’un ni l’autre

Ce n’est pas que l’attente ne fait ni l’un ni l’autre . Rappelez-vous, le but de await n’est pas de rendre le code synchrone comme par magie asynchrone . C’est pour permettre d’ utiliser les mêmes techniques que nous utilisons pour écrire du code synchrone lorsque vous appelez un code asynchrone . Await est de faire en sorte que le code qui utilise des opérations à forte latence ressemble à du code qui utilise des opérations à faible latence . Ces opérations à haute latence peuvent être sur des threads, elles peuvent être sur du matériel spécialisé, elles peuvent être en train de déchirer leur travail en morceaux et de le placer dans la queue des messages pour un traitement ultérieur par le thread d’interface utilisateur. Ils font quelque chose pour atteindre l’asynchronisme, mais ce sont eux qui le font. Attendez vous permet de profiter de cette asynchronie.

En outre, je pense que vous manquez une troisième option. Nous, les personnes âgées – les enfants d’aujourd’hui avec leur musique rap devraient sortir de ma pelouse, etc. – se souvenir du monde de Windows au début des années 1990. Il n’y avait pas de machines multi-processeurs et pas de planificateurs de threads. Vous vouliez exécuter deux applications Windows en même temps, vous deviez céder . Le multitâche était coopératif . Le système d’exploitation indique à un processus qu’il doit exécuter, et s’il est mal conduit, tous les autres processus ne sont plus pris en charge. Il fonctionne jusqu’à ce qu’il cède, et il doit en quelque sorte savoir comment reprendre là où il l’a laissé la prochaine fois que le contrôle du système d’exploitation lui reviendra . Le code asynchrone mono-thread ressemble beaucoup à cela, avec “wait” au lieu de “yield”. En attente signifie “Je vais me souvenir de l’endroit où je me suis arrêté et laisser quelqu’un d’autre courir pendant un moment; rappeler-moi quand la tâche que j’attends est terminée et je reprendrai là où je m’étais arrêté.” Je pense que vous pouvez voir comment cela rend les applications plus réactives, tout comme dans Windows 3 jours.

appeler n’importe quelle méthode signifie attendre que la méthode se termine

Il y a la clé qui vous manque. Une méthode peut revenir avant que son travail ne soit terminé . C’est l’essence même de l’asynchronie. Une méthode retourne, elle retourne une tâche qui signifie “ce travail est en cours, dites-moi ce qu’il faut faire quand il est terminé”. Le travail de la méthode n’est pas terminé, même s’il est revenu .

Avant d’attendre l’opérateur, vous deviez écrire un code qui ressemblait à des spaghettis intégrés à swiss cheese pour faire face au fait que nous avons du travail à faire après la fin, mais avec le retour et l’achèvement de la synchronisation . Await vous permet d’écrire du code qui ressemble à un retour et à l’achèvement de la synchronisation, sans qu’ils soient réellement synchronisés.

Je l’explique en entier dans mon blog Il n’y a pas de fil .

En résumé, les systèmes d’E / S modernes utilisent beaucoup le DMA (Direct Memory Access). Il existe des processeurs dédiés spéciaux sur les cartes réseau, les cartes vidéo, les contrôleurs de disque dur, les ports série / parallèle, etc. Ces processeurs ont un access direct au bus mémoire et gèrent la lecture / écriture de manière totalement indépendante du processeur. L’UC a juste besoin de notifier le périphérique de l’emplacement en mémoire contenant les données, puis peut faire sa propre chose jusqu’à ce que le périphérique déclenche une interruption en avertissant le CPU que la lecture / écriture est terminée.

Une fois que l’opération est en vol, il n’y a plus de travail à faire pour le processeur, donc pas de thread.

Je suis vraiment content que quelqu’un ait posé cette question, car je pensais aussi que les threads étaient nécessaires à la concurrence. Lorsque j’ai vu les boucles d’événement pour la première fois, j’ai pensé qu’elles étaient un mensonge. Je me suis dit “il n’y a pas moyen que ce code soit simultané s’il s’exécute dans un seul thread”. Gardez à l’esprit que c’est après avoir déjà lutté pour comprendre la différence entre la concurrence et le parallélisme.

Après mes recherches, j’ai finalement trouvé la pièce manquante: select() . Plus précisément, le multiplexage IO, implémenté par différents kernelx sous différents noms: select() , poll() , epoll() , kqueue() . Ce sont des appels système qui, bien que les détails de la mise en œuvre diffèrent, vous permettent de transmettre un ensemble de descripteurs de fichiers à surveiller. Ensuite, vous pouvez effectuer un autre appel qui se bloque jusqu’à ce que celui des descripteurs du fichier surveillé change.

Ainsi, on peut attendre un ensemble d’événements IO (la boucle d’événement principale), gérer le premier événement qui se termine, puis céder le contrôle à la boucle d’événement. Rincer et répéter.

Comment cela marche-t-il? Eh bien, la réponse courte est que c’est la magie au niveau du kernel et du matériel. Outre l’ordinateur, il existe de nombreux composants dans un ordinateur et ces composants peuvent fonctionner en parallèle. Le kernel peut contrôler ces périphériques et communiquer directement avec eux pour recevoir certains signaux.

Ces appels système de multiplexage d’E / S constituent le composant fondamental des boucles d’événements à thread unique telles que node.js ou Tornado. Lorsque vous await une fonction, vous surveillez un certain événement (l’achèvement de cette fonction), puis vous cédez le contrôle à la boucle d’événement principale. Lorsque l’événement que vous regardez se termine, la fonction (éventuellement) reprend là où elle s’était arrêtée. Les fonctions permettant de suspendre et de reprendre un calcul comme celui-ci sont appelées coroutines .

await et async utilisent les tâches pas les threads.

Le framework a un pool de threads prêt à exécuter du travail sous la forme d’objects Task ; Soumettre une tâche au pool signifie sélectionner un thread 1 existant et libre pour appeler la méthode d’action de tâche.
Créer une tâche consiste à créer un nouvel object beaucoup plus rapidement que de créer un nouveau thread.

Etant donné qu’une tâche peut être associée à une Continuation , il s’agit d’un nouvel object Task à exécuter une fois le thread terminé.

Depuis async/await utilisation Tâche s ils ne créent pas un nouveau thread.


Bien que la technique de programmation d’interruption soit largement utilisée dans tous les systèmes d’exploitation modernes, je ne pense pas qu’ils soient pertinents ici.
Vous pouvez avoir deux tâches liées au processeur qui s’exécutent en parallèle (entrelacées en fait) dans un seul processeur en utilisant aysnc/await .
Cela ne s’explique pas simplement par le fait que le système d’exploitation prend en charge la mise en queue de l’ IORP .


La dernière fois que j’ai vérifié que le compilateur avait transformé les méthodes async en DFA , le travail est divisé en étapes, chacune se terminant par une instruction d’ await .
L’ await démarre sa tâche et lui associe une suite pour exécuter l’étape suivante.

Comme exemple de concept, voici un exemple de pseudo-code.
Les choses sont simplifiées par souci de clarté et parce que je ne me souviens pas exactement de tous les détails.

 method: instr1 instr2 await task1 instr3 instr4 await task2 instr5 return value 

Il se transforme en quelque chose comme ça

 int state = 0; Task nextStep() { switch (state) { case 0: instr1; instr2; state = 1; task1.addContinuation(nextStep()); task1.start(); return task1; case 1: instr3; instr4; state = 2; task2.addContinuation(nextStep()); task2.start(); return task2; case 2: instr5; state = 0; task3 = new Task(); task3.setResult(value); task3.setCompleted(); return task3; } } method: nextStep(); 

1 En fait, un pool peut avoir sa politique de création de tâche.

Je ne vais pas rivaliser avec Eric Lippert ou Lasse V. Karlsen, et d’autres, je voudrais juste attirer l’attention sur une autre facette de cette question, qui, à mon avis, n’a pas été explicitement mentionnée.

L’utilisation de await sur lui-même ne rend pas votre application réactive comme par magie. Si quoi que vous fassiez dans la méthode que vous attendez des threads de l’interface utilisateur, cela bloquerait toujours votre interface utilisateur de la même manière qu’une version non attendue .

Vous devez écrire spécifiquement votre méthode attendue afin qu’elle génère un nouveau thread ou utilise quelque chose comme un port d’achèvement (qui renverra l’exécution dans le thread en cours et appellera quelque chose pour la continuation chaque fois que le port d’achèvement est signalé). Mais cette partie est bien expliquée dans d’autres réponses.

Voici comment je vois tout cela, ce n’est peut-être pas techniquement précis, mais ça m’aide au moins :).

Il existe essentiellement deux types de traitement (calcul) qui se produisent sur une machine:

  • traitement qui se produit sur le processeur
  • traitement qui se produisent sur d’autres processeurs (GPU, carte réseau, etc.), appelons-les IO.

Donc, quand on écrit un morceau de code source, après la compilation, en fonction de l’object utilisé (et c’est très important), le traitement sera lié au processeur , ou lié aux entréessorties , et en fait, il peut être lié à une combinaison de tous les deux.

Quelques exemples:

  • Si j’utilise la méthode Write de l’object FileStream (qui est un Stream), le traitement sera dit, 1% lié au processeur et 99% lié au IO.
  • Si j’utilise la méthode Write de l’object NetworkStream (qui est un Stream), le traitement sera dit, 1% lié au processeur et 99% IO lié.
  • Si j’utilise la méthode Write de l’object Memorystream (qui est un stream), le traitement sera 100% lié au processeur.

Ainsi, comme vous le voyez, du sharepoint vue d’un programmeur orienté object, bien que j’accède toujours à un object Stream , ce qui se passe en dessous peut dépendre fortement du type ultime de l’object.

Maintenant, pour optimiser les choses, il est parfois utile de pouvoir exécuter du code en parallèle (notez que je n’utilise pas le mot asynchrone) si c’est possible et / ou nécessaire.

Quelques exemples:

  • Dans une application de bureau, je souhaite imprimer un document, mais je ne souhaite pas l’attendre.
  • Mon serveur Web héberge plusieurs clients en même temps, chacun obtenant ses pages en parallèle (non sérialisées).

Avant async / waiting, nous avions essentiellement deux solutions à cela:

  • Fils . Il était relativement facile à utiliser, avec les classes Thread et ThreadPool. Les threads sont uniquement liés au processeur .
  • Le “vieux” modèle de programmation asynchrone Begin / End / AsyncCallback . C’est juste un modèle, il ne vous dit pas si vous serez lié au processeur ou aux entrées-sorties. Si vous regardez les classes Socket ou FileStream, c’est IO, ce qui est cool, mais nous l’utilisons rarement.

L’async / wait n’est qu’un modèle de programmation commun, basé sur le concept de tâche . Il est un peu plus facile à utiliser que les threads ou les pools de threads pour les tâches liées au processeur, et beaucoup plus facile à utiliser que l’ancien modèle Begin / End. Undercovers, cependant, c’est “juste” un wrapper de fonctionnalités complet et sophistiqué sur les deux.

Donc, le vrai gain réside principalement dans les tâches liées aux entrées-sorties , tâche qui n’utilise pas le processeur, mais async / wait n’est toujours qu’un modèle de programmation, cela ne vous aide pas à déterminer comment et où le traitement se produira.

Cela signifie que ce n’est pas parce qu’une classe a une méthode “DoSomethingAsync” renvoyant un object Task que vous pouvez supposer qu’il sera lié au CPU (ce qui signifie qu’il peut être tout à fait inutile , surtout s’il n’a pas de paramètre d’annulation) ou IO Bound (ce qui signifie que c’est probablement un must ), ou une combinaison des deux (puisque le modèle est très viral, la liaison et les avantages potentiels peuvent être, au final, super mixtes et pas si évidents).

Donc, en revenant à mes exemples, faire mes opérations d’écriture en utilisant async / waiting sur MemoryStream restra lié au processeur (je n’en bénéficierai probablement pas), bien que je vais sûrement en bénéficier avec les fichiers et les stream réseau.

Résumer les autres réponses:

Async / wait est principalement créé pour les tâches liées aux E / S depuis leur utilisation, on peut éviter de bloquer le thread appelant.

Dans le cas de tâches liées aux E / S, le principal avantage est d’éviter de bloquer le thread d’interface utilisateur. Pour les threads non-UI, on pourrait avoir des avantages en termes de performances.

Async ne crée pas son propre thread. Le thread de la méthode appelante est utilisé pour exécuter la méthode asynchrone jusqu’à ce qu’il trouve une attente. Le même thread continue alors à exécuter le rest de la méthode appelante au-delà de l’appel de méthode asynchrone. Dans la méthode async appelée, après le retour de l’attendable, la continuation peut être exécutée sur un thread à partir du pool de threads – le seul endroit où un thread séparé apparaît dans l’image.

En fait, les chaînes d’ async await sont des machines d’état générées par le compilateur CLR.

async await cependant utilise des threads que TPL utilise un pool de threads pour exécuter des tâches.

La raison pour laquelle l’application n’est pas bloquée est que la machine d’état peut décider quelle co-routine exécuter, répéter, vérifier et décider à nouveau.

Lectures complémentaires:

Qu’est-ce qu’async et wait générer?

Async Attend et la StateMachine générée

Asynchrone C # et F # (III.): Comment ça marche? – Tomas Pesortingcek

Modifier :

D’accord. Il semble que mon élaboration soit incorrecte. Cependant, je dois souligner que les machines d’état sont des atouts importants pour les async await . Même si vous intégrez des E / S asynchrones, vous avez toujours besoin d’un assistant pour vérifier si l’opération est terminée. Nous avons donc toujours besoin d’une machine à états pour déterminer quelle routine peut être exécutée de manière synchrone.