Si async-wait ne crée pas de threads supplémentaires, comment cela rend-il les applications réactives?

242

Maintes et maintes fois, je vois qu'il est dit que l'utilisation de async- awaitne 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- awaitne 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.

Mme Corlib
la source
17
Les tâches d'E / S ne sont pas liées au processeur et ne nécessitent donc pas de thread. Le point principal de l'async est de ne pas bloquer les threads pendant les tâches liées aux IO.
juharr
24
@jdweng: Non, pas du tout. Même s'il a créé de nouveaux threads , c'est très différent de la création d'un nouveau processus.
Jon Skeet
8
Si vous comprenez la programmation asynchrone basée sur le rappel, vous comprenez comment await/ asyncfonctionne sans créer de threads.
user253751
6
Il n'a pas exactement faire une application plus réactive, mais il ne vous décourage pas de bloquer vos fils, ce qui est une cause fréquente d'applications non répondeurs.
Owen
6
@RubberDuck: Oui, il peut utiliser un thread du pool de threads pour la suite. Mais cela ne démarre pas un thread de la manière que l'OP imagine ici - ce n'est pas comme s'il était dit "Prenez cette méthode ordinaire, exécutez-la maintenant dans un thread séparé - là, c'est asynchrone." C'est beaucoup plus subtil que ça.
Jon Skeet

Réponses:

299

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:

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 tout ce qu'il GetSomethingAsyncretourne 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:

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 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_Clickmé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/awaitne 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:

  1. Tout le code menant à await, y compris l'appel àGetSomethingAsync
  2. Tout le code suivant await

Illustration:

code... code... code... await X(); ... code... code... code...

Réorganisé:

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 jusqu'à await
  2. Il appelle la GetSomethingAsyncméthode, qui fait son travail, et renvoie quelque chose qui se terminera 2 secondes à l'avenir

    Jusqu'à 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 à awaitprend beaucoup de temps, l'interface utilisateur se figera toujours. Dans notre exemple, pas tellement

  3. Ce que le awaitmot - 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.

  4. 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.

  5. 2 secondes plus tard, la chose que nous attendons se termine et ce qui se passe maintenant 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 pour vous d'exécuter ", et ce code est tout le code après l'attente.
  6. Lorsque la boucle de message parvient à ce message, elle "retapera" essentiellement cette méthode là où elle s'était arrêtée, juste après awaitet 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'utiliser async/awaitcorrectement, il bloquera à nouveau la boucle de message

Il 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 GetSomethingAsynctourne 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:

  • Demandes Web (et bien d'autres choses liées au réseau qui prennent du temps)
  • Lecture et écriture de fichiers asynchrones
  • et bien d'autres, un bon signe est que la classe / interface en question a des méthodes nommées SomethingSomethingAsyncou BeginSomethinget EndSomethinget qu'il y en ait une IAsyncResult.

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.

Lasse V. Karlsen
la source
11
C'est donc essentiellement ce que l'OP décrit comme " Simuler l'exécution parallèle en planifiant des tâches et en basculant entre elles ", n'est-ce pas?
Bergi
4
@Bergi Pas tout à fait. L'exécution est vraiment parallèle - la tâche d'E / S asynchrone est en cours et ne nécessite pas de threads pour continuer (c'est quelque chose qui a été utilisé bien avant l'arrivée de Windows - MS DOS a également utilisé des E / S asynchrones, même si ce n'était pas le cas avoir multi-threading!). Bien sûr, vous 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.
Luaan
3
C'est pourquoi je voulais explicitement éviter de trop parler de ce que cette méthode a fait, car la question portait spécifiquement sur async / wait, qui ne crée pas ses propres threads. De toute évidence, ils peuvent être utilisés pour attendre la fin des threads.
Lasse V. Karlsen
6
@ LasseV.Karlsen - J'ingère votre excellente réponse, mais je reste accroché à un détail. Je comprends que le gestionnaire d'événements existe, comme à l'étape 4, ce qui permet à la pompe de message de continuer à pomper, mais quand et la «chose qui prend deux secondes» continue-t-elle à s'exécuter si ce n'est sur un thread séparé? S'il devait s'exécuter sur le thread d'interface utilisateur, il bloquerait de toute façon la pompe de message pendant son exécution car il doit s'exécuter un certain temps sur le même thread .. [suite] ...
rory.ap
3
J'aime votre explication avec la pompe à messages. En quoi votre explication diffère-t-elle lorsqu'il n'y a pas de pompe de messages comme dans l'application console ou le serveur Web? Comment se réalise la recentrage d'une méthode?
Puchacz
95

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.

Stephen Cleary
la source
Juste pour être clair .. Je comprends le niveau élevé de ce qui se passe lors de l'utilisation de async-wait. Concernant la création sans thread - il n'y a pas de thread uniquement dans les requêtes d'E / S vers des périphériques qui, comme vous l'avez dit, ont leurs propres processeurs qui gèrent la requête elle-même? Pouvons-nous supposer que TOUTES les demandes d'E / S sont traitées sur de tels processeurs indépendants, ce qui signifie utiliser Task.Run UNIQUEMENT sur les actions liées au processeur?
Yonatan Nir du
@YonatanNir: Il ne s'agit pas seulement de processeurs séparés; tout type de réponse événementielle est naturellement asynchrone. Task.Runest le plus approprié pour les actions liées au processeur , mais il a également une poignée d'autres utilisations.
Stephen Cleary
1
J'ai fini de lire votre article et il y a encore quelque chose de basique que je ne comprends pas car je ne connais pas vraiment l'implémentation de niveau inférieur de l'OS. J'ai obtenu ce que vous avez écrit à l'endroit où vous avez écrit: "L'opération d'écriture est maintenant" en cours ". Combien de threads la traitent? Aucun." . Donc, s'il n'y a pas de threads, comment l'opération se fait-elle sinon sur un thread?
Yonatan Nir
6
C'est la pièce manquante dans des milliers d'explications !!! Il y a en fait quelqu'un qui fait le travail en arrière-plan avec les opérations d'E / S. Ce n'est pas un thread mais un autre composant matériel dédié qui fait son travail!
the_dark_destructor
2
@PrabuWeerasinghe: Le compilateur crée une structure qui contient les variables d'état et locales. Si un attente doit céder (c'est-à-dire retourner à son appelant), cette structure est encadrée et vit sur le tas.
Stephen Cleary
87

la seule façon dont un ordinateur peut sembler faire plus d'une chose à la fois est (1) de faire plus d'une chose à la fois, (2) de la simuler en planifiant des tâches et en basculant entre elles. Donc, si asynchrone n'attend aucun de ces

Ce n'est pas qu'attendre non plus . N'oubliez pas que le but de awaitn'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.

appeler une méthode signifie attendre la fin de la méthode

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.

Eric Lippert
la source
D'autres langages modernes de haut niveau prennent également en charge un comportement coopératif explicite similaire (c.-à-d. Que la fonction fait certaines choses, produit [peut-être l'envoi de valeur / objet à l'appelant], continue là où elle s'était arrêtée lorsque le contrôle est rendu [éventuellement avec une entrée supplémentaire fournie] ). Les générateurs sont très gros en Python, pour une chose.
JAB
2
@JAB: Absolument. Les générateurs sont appelés "blocs itérateurs" en C # et utilisent le yieldmot - clé. Les asyncmé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.
Eric Lippert
1
L'analogie avec le rendement est bonne - c'est le multitâche coopératif dans un même processus. (et évitant ainsi les problèmes de stabilité du système du multitâche coopératif à l'échelle du système)
user253751
3
Je pense que le concept d '"interruptions du processeur" utilisé pour les E / S n'est pas connu de beaucoup de "programmeurs" de modem, par conséquent, ils pensent qu'un thread doit attendre chaque bit d'E / S.
Ian Ringrose
La méthode @EricLippert Async de WebClient crée en fait du fil supplémentaire, voir ici stackoverflow.com/questions/48366871/…
KevinBui
28

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 awaitune 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 .

gardenhead
la source
25

awaitet asyncutilisez 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/awaitutilisation 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 asyncméthodes transformées par le compilateur en DFA , le travail est divisé en étapes, chacune se terminant par une awaitinstruction.
leawait 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.

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âches.

Margaret Bloom
la source
16

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 awaitseul 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.

Andrew Savinykh
la source
3
Ce n'est pas une compétition en premier lieu; c'est une collaboration!
Eric Lippert
16

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:

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

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:

  • si j'utilise la méthode Write de l' FileStreamobjet (qui est un Stream), le traitement sera, par exemple, lié à 1% au processeur et à 99% aux E / S.
  • si j'utilise la méthode Write de l' NetworkStreamobjet (qui est un Stream), le traitement sera, par exemple, lié à 1% au processeur et à 99% aux E / S.
  • si j'utilise la méthode Write de l' Memorystreamobjet (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 Streamobjet, 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:

  • Dans une application de bureau, je veux imprimer un document, mais je ne veux 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 / wait, 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 CPU .
  • L'ancien modèle de programmation asynchrone Begin / End / AsyncCallback . C'est juste un modèle, il ne vous dit pas si vous serez lié au CPU ou aux IO. Si vous jetez un œil aux classes Socket ou FileStream, c'est lié aux IO, ce qui est cool, mais nous l'utilisons rarement.

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.

Simon Mourier
la source
1
C'est une assez bonne réponse en utilisant le theadpool pour le travail lié au processeur est médiocre dans le sens où les threads TP doivent être utilisés pour décharger les opérations d'E / S. Imo de travail lié au CPU devrait être bloquant avec des mises en garde bien sûr et rien n'empêche l'utilisation de plusieurs threads.
davidcarr
3

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.

vaibhav kumar
la source
Bon résumé, mais je pense qu'il devrait répondre à 2 autres questions afin de donner une image complète: 1. Sur quel thread le code attendu est exécuté? 2. Qui contrôle / configure le pool de threads mentionné - le développeur ou l'environnement d'exécution?
stojke
1. Dans ce cas, le code attendu est principalement une opération liée aux E / S qui n'utiliserait pas de threads CPU. Si vous souhaitez utiliser l'attente pour une opération liée au processeur, une tâche distincte peut être générée. 2. Le thread dans le pool de threads est géré par le Planificateur de tâches qui fait partie du framework TPL.
vaibhav kumar
2

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:

while (getMessage(out message)) // pseudo-code
{
   dispatchMessage(message); // pseudo-code
}

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?

void expensiveOperation()
{
    for (int i = 0; i < 1000; i++)
    {
        Thread.Sleep(10);
    }
}

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:

void expensiveOperation()
{
    doIteration(0);
}

void doIteration(int i)
{
    if (i >= 1000) return;
    Thread.Sleep(10); // Do a piece of work.
    postFunctionCallMessage(() => {doIteration(i + 1);}); // Pseudo code. 
}

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. postFunctionCallMessageSi 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 Taskclasse. 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). La Taskclasse 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 .ContinueWithgâ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 que awaitvous 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.

Calmarius
la source
0

En fait, les async awaitchaî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 awaitart. 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.

Steve Fan
la source
0

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

Blechdose
la source