Maintes et maintes fois, je vois qu'il est dit que l'utilisation de async
- await
ne crée pas de threads supplémentaires. Cela n'a pas de sens car la seule façon dont un ordinateur peut sembler faire plus d'une chose à la fois est
- Faire plus d'une chose à la fois (exécuter en parallèle, utiliser plusieurs processeurs)
- La simuler en planifiant des tâches et en basculant entre elles (faire un peu de A, un peu de B, un peu de A, etc.)
Donc, si async
- await
ne fait rien de tout cela, alors comment peut-il rendre une application sensible? S'il n'y a qu'un seul thread, alors appeler n'importe quelle méthode signifie attendre que la méthode soit terminée avant de faire quoi que ce soit d'autre, et les méthodes à l'intérieur de cette méthode doivent attendre le résultat avant de continuer, et ainsi de suite.
c#
.net
multithreading
asynchronous
async-await
Mme Corlib
la source
la source
await
/async
fonctionne sans créer de threads.Réponses:
En fait, asynchrone / attendre 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 de bouton dans une application Windows Forms:
Je vais explicitement ne pas parler de tout ce qu'il
GetSomethingAsync
retourne pour l'instant. 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 de bouton ressemblerait à ceci:
Lorsque vous cliquez sur le bouton du formulaire, l'application semble se bloquer pendant environ 2 secondes, pendant que nous attendons que cette méthode soit terminée. Ce qui se passe, c'est que la "pompe à messages", essentiellement une boucle, est bloquée.
Cette boucle demande continuellement aux fenêtres "Quelqu'un a-t-il fait quelque chose, comme avoir déplacé la souris, cliqué sur quelque chose? Dois-je 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 "button1" (ou le type de message équivalent de Windows) et a fini par appeler notre
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 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 pendant une seconde, elle est rapidement visible 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 à l'écart, une rafale de messages de peinture est envoyée à votre programme indiquant quelle région de la fenêtre qui est maintenant 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, c'est que votre méthode est divisée en deux. C'est l'un de ces types de sujets généraux, donc je n'entrerai pas dans trop de détails, mais il suffit de dire que la méthode est divisée en ces deux choses:
await
, y compris l'appel àGetSomethingAsync
await
Illustration:
Réorganisé:
Fondamentalement, la méthode s'exécute comme ceci:
await
Il appelle la
GetSomethingAsync
méthode, qui fait son travail, et renvoie quelque chose qui se terminera 2 secondes à l'avenirJusqu'à présent, nous sommes toujours dans l'appel d'origine à button1_Click, qui se produit sur le thread principal, appelé à partir de la boucle de message. Si le code menant à
await
prend beaucoup de temps, l'interface utilisateur se figera toujours. Dans notre exemple, pas tellementCe que le
await
mot - clé, avec une magie de compilateur intelligent, fait, c'est essentiellement quelque chose comme "Ok, vous savez quoi, je vais simplement revenir du gestionnaire d'événements de clic de bouton ici. Lorsque vous (comme dans, la chose que nous" re en attente de) terminer, faites le moi savoir car il me reste du code à exécuter ".En fait, il indiquera à la classe SynchronizationContext que c'est fait, ce qui, selon le contexte de synchronisation réel en cours en ce moment, sera mis en file d'attente pour exécution. La classe de contexte utilisée dans un programme Windows Forms la mettra en file d'attente à l'aide de la file d'attente que la boucle de messages pompe.
Il revient donc à la boucle des messages, qui est maintenant libre de continuer à pomper les 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, traitant les autres clics sur les boutons, redimensionnant et, plus important encore, redessinant , de sorte qu'elle ne semble pas se figer.
await
et continuera à exécuter le reste 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 l'utiliserasync/await
correctement, il bloquera à nouveau la boucle de messageIl y a beaucoup de pièces mobiles sous le capot ici, donc 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 limitations et problèmes sous-jacents continuent de fuir dans le code environnant, et s'ils ne le font pas, vous finissez généralement par devoir déboguer une application qui se casse au hasard pour apparemment aucune bonne raison.
OK, et si
GetSomethingAsync
tourne un fil qui se terminera en 2 secondes? Oui, alors évidemment, il y a un nouveau fil en jeu. Ce thread, cependant, n'est pas à cause de l'async-ness 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 différentes choses.async/await
par eux - mêmes ne font pas tourner de nouveaux threads mais évidemment les "choses que nous attendons" peuvent ê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 de leur propre chef mais sont toujours asynchrones:
SomethingSomethingAsync
ouBeginSomething
etEndSomething
et qu'il y en ait uneIAsyncResult
.Habituellement, ces choses n'utilisent pas de fil sous le capot.
OK, donc vous voulez une partie de ce "sujet large"?
Eh bien, demandons à Try Roslyn 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 un truc assez sanglant.
la source
await
pouvez également l'utiliser comme vous le décrivez, mais ce n'est généralement pas le cas. Seuls les rappels sont planifiés (sur le pool de threads) - entre le rappel et la demande, aucun thread n'est nécessaire.Je l'explique en détail dans mon article de blog Il n'y a pas de fil .
En résumé, les systèmes d'E / S modernes font un usage intensif du 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 HDD, les ports série / parallèle, etc. Ces processeurs ont un accès direct au bus mémoire et gèrent la lecture / écriture de manière complètement indépendante du CPU. Le CPU a juste besoin de notifier le périphérique de l'emplacement dans la mémoire contenant les données, puis peut faire sa propre chose jusqu'à ce que le périphérique déclenche une interruption informant le CPU que la lecture / écriture est terminée.
Une fois que l'opération est en vol, il n'y a pas de travail à faire pour le CPU, et donc pas de thread.
la source
Task.Run
est le plus approprié pour les actions liées au processeur , mais il a également une poignée d'autres utilisations.Ce n'est pas qu'attendre non plus . N'oubliez pas que le but de
await
n'est pas de rendre le code synchrone comme asynchrone par magie . C'est pour permettre d' utiliser les mêmes techniques que nous utilisons pour écrire du code synchrone lors de l'appel en code asynchrone . Attendre consiste à faire ressembler le code qui utilise des opérations à latence élevée au code qui utilise des opérations à latence élevée . Ces opérations à latence élevée peuvent être sur des threads, elles peuvent être sur du matériel spécial, elles peuvent déchirer leur travail en petits morceaux et le mettre dans la file d'attente de messages pour le traitement par le thread d'interface utilisateur plus tard. Ils font quelque chose pour réaliser l'asynchronie, mais ilssont ceux qui le font. Attendre vous permet simplement de profiter de cette asynchronie.De plus, je pense qu'il vous manque une troisième option. Nous les personnes âgées - les enfants d'aujourd'hui avec leur musique rap devraient descendre de ma pelouse, etc. - nous souvenons du monde de Windows au début des années 1990. Il n'y avait pas de machines multi-CPU 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 s'exécuter, et s'il ne se comporte pas correctement, il empêche tous les autres processus d'être servis. Il s'exécute jusqu'à ce qu'il cède, et d'une manière ou d'une autre il doit savoir comment reprendre là où il s'était arrêté la prochaine fois que les mains du système d'exploitation lui reviendront. Le code asynchrone à un seul thread ressemble beaucoup à cela, avec "attendre" au lieu de "céder". Attendre signifie "Je vais me souvenir où je me suis arrêté ici, et laisser quelqu'un d'autre courir pendant un moment; rappelez-moi lorsque la tâche que j'attends est terminée, et je reprendrai là où je me suis arrêté." Je pense que vous pouvez voir comment cela rend les applications plus réactives, comme c'était le cas sous Windows 3 jours.
Il y a la clé qui vous manque. Une méthode peut revenir avant la fin de son travail . C'est l'essence même de l'asynchronie. Une méthode revient, elle renvoie une tâche qui signifie "ce travail est en cours; dites-moi quoi faire quand il sera terminé". Le travail de la méthode n'est pas terminé, même s'il est revenu .
Avant l'opérateur d'attente, vous deviez écrire du code qui ressemblait à des spaghettis enfilés dans du fromage suisse pour faire face au fait que nous avons du travail à faire après la fin, mais avec le retour et la fin désynchronisés . Await vous permet d'écrire du code qui ressemble au retour et à l'achèvement sont synchronisés, sans qu'ils soient réellement synchronisés.
la source
yield
mot - clé. Lesasync
méthodes et les itérateurs en C # sont une forme de coroutine , qui est le terme général pour une fonction qui sait suspendre son fonctionnement actuel pour une reprise ultérieure. Un certain nombre de langues ont des coroutines ou des flux de contrôle de type coroutine de nos jours.Je suis vraiment content que quelqu'un ait posé cette question, car pendant longtemps j'ai également cru que les threads étaient nécessaires pour la concurrence. Quand j'ai vu des boucles d'événement pour la première fois , j'ai pensé que c'était un mensonge. Je me suis dit "il n'y a aucun moyen que ce code soit simultané s'il s'exécute sur un seul thread". Gardez à l'esprit que c'est après avoir déjà traversé la lutte pour comprendre la différence entre la concurrence et le parallélisme.
Après la recherche de moi - même, j'ai finalement trouvé la pièce manquante:
select()
. Plus précisément, le multiplexage IO, mis en œuvre par divers noyaux sous différents noms:select()
,poll()
,epoll()
,kqueue()
. Ce sont des appels système qui, bien que les détails d'implémentation diffèrent, vous permettent de transmettre un ensemble de descripteurs de fichiers à surveiller. Ensuite, vous pouvez effectuer un autre appel qui bloque jusqu'à ce que l'un des descripteurs de fichiers surveillés change.Ainsi, on peut attendre un ensemble d'événements d'E / S (la boucle d'événements principale), gérer le premier événement qui se termine, puis redonner le contrôle à la boucle d'événements. Rincez et répétez.
Comment cela marche-t-il? Eh bien, la réponse courte est que c'est une magie au niveau du noyau et du matériel. Il y a de nombreux composants dans un ordinateur en plus du CPU, et ces composants peuvent fonctionner en parallèle. Le noyau peut contrôler ces périphériques et communiquer directement avec eux pour recevoir certains signaux.
Ces appels système de multiplexage IO sont le bloc de construction fondamental des boucles d'événements à un seul thread comme node.js ou Tornado. Lorsque vous
await
une fonction, vous surveillez un certain événement (l'achèvement de cette fonction), puis redonner le contrôle à la boucle d'événement principal. Lorsque l'événement que vous regardez se termine, la fonction reprend (finalement) là où elle s'était arrêtée. Les fonctions qui vous permettent de suspendre et de reprendre le calcul comme celui-ci sont appelées coroutines .la source
await
etasync
utilisez Tâches et non Threads.Le framework dispose d'un pool de threads prêts à exécuter certains travaux sous la forme d' objets Task ; soumettre une tâche au pool signifie sélectionner un thread libre, déjà existant 1 , pour appeler la méthode d'action de tâche.
La création d'une tâche consiste à créer un nouvel objet, bien plus rapidement que la création d'un nouveau thread.
Étant donné une tâche, il est possible de joindre un continuation , il s'agit d'un nouvel objet de tâche à exécuter une fois le thread terminé.
Depuis l'
async/await
utilisation des tâches, ils ne le font pas créent nouveau thread.Bien que les techniques de programmation d'interruption soient largement utilisées dans tous les systèmes d'exploitation modernes, je ne pense pas qu'elles soient pertinentes ici.
Vous pouvez avoir deux tâches liées au processeur exécutées en parallèle (entrelacées en fait) dans un seul processeur utilisant
aysnc/await
.Cela ne pouvait pas être expliqué simplement par le fait que la mise en file d'attente de prise en charge du système d'exploitation IORP .
La dernière fois que j'ai vérifié les
async
méthodes transformées par le compilateur en DFA , le travail est divisé en étapes, chacune se terminant par uneawait
instruction.le
await
démarre sa tâche et lui attache une suite pour exécuter l'étape suivante.À titre d'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.
Il se transforme en quelque chose comme ça
1 En fait, un pool peut avoir sa politique de création de tâches.
la source
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 je pense n'a pas été explicitement mentionnée.
Utiliser
await
seul ne rend pas votre application comme par magie. Si tout ce que vous faites dans la méthode que vous attendez des blocs de threads de l'interface utilisateur, il bloquera toujours votre interface utilisateur de la même manière que le ferait une version non attendue .Vous devez écrire votre méthode attendue spécifiquement pour 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 actuel et appellera quelque chose d'autre pour la poursuite chaque fois que le port d'achèvement est signalé). Mais cette partie est bien expliquée dans d'autres réponses.
la source
Voici comment je vois tout cela, ce n'est peut-être pas super précis techniquement mais ça m'aide, au moins :).
Il existe essentiellement deux types de traitement (calcul) qui se produisent sur une machine:
Ainsi, lorsque nous écrivons un morceau de code source, après la compilation, en fonction de l'objet que nous utilisons (et cela est très important), le traitement sera lié au processeur ou lié aux E / S , et en fait, il peut être lié à une combinaison de tous les deux.
Quelques exemples:
FileStream
objet (qui est un Stream), le traitement sera, par exemple, lié à 1% au processeur et à 99% aux E / S.NetworkStream
objet (qui est un Stream), le traitement sera, par exemple, lié à 1% au processeur et à 99% aux E / S.Memorystream
objet (qui est un Stream), le traitement sera lié à 100% au CPU.Donc, comme vous le voyez, du point de vue d'un programmeur orienté objet, bien que j'accède toujours à un
Stream
objet, ce qui se passe en dessous peut dépendre fortement du type ultime de l'objet.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:
Avant async / wait, nous avions essentiellement deux solutions à cela:
L'async / attente n'est qu'un modèle de programmation commun, basé sur le concept de tâche . C'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 complet de fonctionnalités super sophistiqué sur les deux.
Donc, la vraie victoire est principalement sur les tâches liées aux IO , tâches qui n'utilisent pas le CPU, mais async / wait n'est toujours qu'un modèle de programmation, cela ne vous aide pas à déterminer comment / où le traitement se produira à la fin.
Cela signifie que ce n'est pas parce qu'une classe a une méthode "DoSomethingAsync" renvoyant un objet Task que vous pouvez supposer qu'il sera lié au processeur (ce qui signifie qu'il peut être tout à fait inutile , surtout si elle n'a pas de paramètre de jeton d'annulation), ou IO Bound (ce qui signifie que c'est probablement un must ), ou une combinaison des deux (puisque le modèle est assez viral, la liaison et les avantages potentiels peuvent être, en fin de compte, super mélangés et pas si évidents).
Donc, pour en revenir à mes exemples, faire mes opérations d'écriture en utilisant async / wait sur MemoryStream restera lié au processeur (je n'en bénéficierai probablement pas), bien que j'en profiterai certainement avec les fichiers et les flux réseau.
la source
Résumant d'autres réponses:
Async / Wait est principalement créé pour les tâches liées aux E / S car en les utilisant, on peut éviter de bloquer le thread appelant. Leur utilisation principale est avec les threads d'interface utilisateur où il n'est pas souhaité que le thread soit bloqué lors d'une opération liée aux E / S.
Async ne crée pas son propre thread. Le thread de la méthode appelante est utilisé pour exécuter la méthode async jusqu'à ce qu'il trouve un fichier attendu. Le même thread continue ensuite d'exécuter le reste de la méthode appelante au-delà de l'appel de méthode async. Dans la méthode async appelée, après son retour de l'attendu, la poursuite peut être exécutée sur un thread du pool de threads - le seul endroit où un thread séparé entre en image.
la source
J'essaie de l'expliquer de bas en haut. Peut-être que quelqu'un le trouve utile. J'étais là, fait ça, réinventé, quand on faisait des jeux simples en DOS en Pascal (bon vieux temps ...)
Donc ... Dans une application pilotée par chaque événement, il y a une boucle d'événement à l'intérieur qui ressemble à ceci:
Les cadres vous cachent généralement ce détail, mais il est là. La fonction getMessage lit l'événement suivant dans la file d'attente d'événements ou attend qu'un événement se produise: déplacement de la souris, keydown, keyup, click, etc. Et dispatchMessage distribue ensuite l'événement au gestionnaire d'événements approprié. Attend ensuite l'événement suivant et ainsi de suite jusqu'à ce qu'un événement quit arrive qui ferme les boucles et termine l'application.
Les gestionnaires d'événements doivent s'exécuter rapidement afin que la boucle d'événements puisse interroger d'autres événements et que l'interface utilisateur reste réactive. Que se passe-t-il si un clic sur un bouton déclenche une opération coûteuse comme celle-ci?
Eh bien, l'interface utilisateur ne répond plus jusqu'à ce que l'opération de 10 secondes se termine alors que le contrôle reste dans la fonction. Pour résoudre ce problème, vous devez diviser la tâche en petites parties pouvant s'exécuter rapidement. Cela signifie que vous ne pouvez pas gérer le tout en un seul événement. Vous devez effectuer une petite partie du travail, puis publier un autre événement dans la file d'attente des événements pour demander la poursuite.
Vous changeriez donc ceci en:
Dans ce cas, seule la première itération s'exécute, puis il poste un message dans la file d'attente des événements pour exécuter l'itération suivante et retourne.
postFunctionCallMessage
Si notre exemple de pseudo-fonction place un événement "appeler cette fonction" dans la file d'attente, le répartiteur d'événements l'appellera quand il l'atteindra. Cela permet de traiter tous les autres événements de l'interface graphique tout en exécutant en continu des morceaux d'un travail de longue durée.Tant que cette tâche longue est en cours d'exécution, son événement de continuation est toujours dans la file d'attente d'événements. Vous avez donc essentiellement inventé votre propre planificateur de tâches. Où les événements de continuation dans la file d'attente sont des "processus" en cours d'exécution. En fait, c'est ce que font les systèmes d'exploitation, sauf que l'envoi des événements de continuation et le retour à la boucle du planificateur se font via l'interruption du temporisateur du CPU où le système d'exploitation a enregistré le code de changement de contexte, vous n'avez donc pas besoin de vous en soucier. Mais ici, vous écrivez votre propre planificateur, vous devez donc vous en soucier - jusqu'à présent.
Nous pouvons donc exécuter des tâches de longue durée dans un seul thread parallèle à l'interface graphique en les décomposant en petits morceaux et en envoyant des événements de continuation. Telle est l'idée générale de la
Task
classe. Il représente une œuvre et lorsque vous l'appelez.ContinueWith
, vous définissez la fonction à appeler comme pièce suivante lorsque la pièce en cours se termine (et sa valeur de retour est transmise à la suite). LaTask
classe utilise un pool de threads, où il y a une boucle d'événements dans chaque thread en attente de faire des travaux similaires à ceux que j'ai montrés au début. De cette façon, vous pouvez exécuter des millions de tâches en parallèle, mais seulement quelques threads pour les exécuter. Mais cela fonctionnerait aussi bien avec un seul thread - tant que vos tâches sont correctement divisées en petits morceaux, chacun d'entre eux semble fonctionner en parallèle.Mais faire tout ce chaînage en divisant le travail en petits morceaux manuellement est un travail fastidieux et gâche totalement la disposition de la logique, car tout le code de tâche d'arrière-plan est fondamentalement un
.ContinueWith
gâchis. C'est donc là que le compilateur vous aide. Il fait tout ce chaînage et cette continuation pour vous en arrière-plan. Lorsque vous dites queawait
vous dites, dites au compilateur que "arrêtez-vous ici, ajoutez le reste de la fonction en tant que tâche de continuation". Le compilateur s'occupe du reste, vous n'avez donc pas à le faire.la source
En fait, les
async await
chaînes sont des machines à états générées par le compilateur CLR.async await
utilise cependant 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 de la co-routine à exécuter, répéter, vérifier et décider à nouveau.
Lectures complémentaires:
Que génère async & wait?
Async Await et le Generated StateMachine
C # et F # asynchrones (III.): Comment ça marche? - Tomas Petricek
Modifier :
D'accord. Il semble que mon élaboration soit incorrecte. Cependant, je dois souligner que les machines d'État sont des atouts importants pour l'
async await
art. Même si vous prenez en charge les E / S asynchrones, vous avez toujours besoin d'un assistant pour vérifier si l'opération est terminée.la source
Cela ne répond pas directement à la question, mais je pense que c'est une information supplémentaire intéressante:
Asynchroniser et attendre ne crée pas de nouveaux threads par lui-même. MAIS selon l'endroit où vous utilisez async d'attente, la partie synchrone AVANT l'attente peut s'exécuter sur un thread différent de la partie synchrone APRÈS l'attente (par exemple, ASP.NET et ASP.NET core se comportent différemment).
Dans les applications basées sur UI-Thread (WinForms, WPF), vous serez sur le même thread avant et après. Mais lorsque vous utilisez async away sur un thread de pool de threads, le thread avant et après l'attente peut ne pas être le même.
Une superbe vidéo sur ce sujet
la source