web-dev-qa-db-fra.com

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

À maintes reprises, je vois qu'il est dit que l'utilisation de async-await ne crée pas de threads supplémentaires. Cela n’a aucun sens car la seule façon pour un ordinateur de sembler faire plus d’une chose à la fois est

  • Effectuer plus d’une tâche à la fois (exécution parallèle, utilisation de plusieurs processeurs)
  • Simulez-le en planifiant des tâches et en basculant entre elles (faites un peu de A, un peu de B, un peu de A, etc.)

Donc, si async-await ne les utilise pas, comment peut-elle rendre une application sensible? S'il n'y a qu'un seul thread, appeler une méthode signifie attendre que la méthode soit terminée avant de faire quoi que ce soit d'autre. Les méthodes à l'intérieur de cette méthode doivent attendre le résultat avant de continuer, et ainsi de suite.

209
Ms. Corlib

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

Nous allons aborder 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 quoi que ce soit GetSomethingAsync revient pour maintenant. 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 dans le formulaire, l'application semble se figer pendant environ 2 secondes, en attendant que cette méthode soit terminée. Qu'est-ce qui se passe est que la "pompe de message", essentiellement une boucle, est bloquée.

Cette boucle demande en permanence à Windows "Est-ce que quelqu'un a fait quelque chose, comme déplacer la souris, cliqué sur quelque chose? Ai-je besoin de repeindre quelque chose? Si oui, dites-le moi!" puis traite ce "quelque chose". Cette boucle a reçu un message indiquant que l'utilisateur avait cliqué sur "bouton1" (ou le type de message équivalent de Windows), et avait fini par appeler notre méthode button1_Click ci-dessus. Jusqu'au retour de cette méthode, 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 en utilisant des messages, ce qui signifie que si la boucle de messages cesse de pomper des messages, même pour une seconde, elle est rapidement perceptible par l'utilisateur. Par exemple, si vous déplacez le bloc-notes ou tout autre programme par-dessus votre propre programme, puis de nouveau à distance, une série de messages de Paint sont envoyés à votre programme pour indiquer la région de la fenêtre qui est redevenue visible. Si la boucle de messages qui traite ces messages est en attente de quelque chose, bloquée, aucun travail de peinture n'est effectué.

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, donc je n’entrerai pas dans les détails, mais je dirai simplement que la méthode est divisée en 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é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 jusqu'à await
  2. Il appelle la méthode GetSomethingAsync, qui fait son travail, et renvoie quelque chose qui se terminera 2 secondes plus tard

    Jusqu'à présent, nous sommes toujours dans l'appel d'origine de 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 sera toujours figée. Dans notre exemple, pas tellement

  3. Le mot-clé await, associé à une magie astucieuse du compilateur, c’est quelque chose du genre "Ok, tu sais quoi, je vais simplement revenir du gestionnaire d’événements clic ici. Lorsque , ce que nous attendons) achever, dites-le moi car il me reste encore du code à exécuter ".

    En fait, il laissera savoir à la classe SynchronizationContext == que, selon le contexte de synchronisation actuellement utilisé, il sera mis en attente d'exécution. La classe de contexte utilisée dans un programme Windows Forms le mettra en file d'attente à l'aide de la file d'attente que la boucle de messages est en train de pomper.

  4. Donc, il revient à la boucle de messages, qui est maintenant libre de continuer à afficher 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, traite d'autres clics de bouton, redimensionne et surtout, redessine , ne semble pas se figer.

  5. 2 secondes plus tard, la chose que nous attendons est terminée et ce qui se passe maintenant, c’est qu’il place un message dans la file d’attente de la boucle de messages en disant: "Hé, j’ai du code supplémentaire pour vous exécuter ", et ce code est tout le code après l'attente.
  6. Lorsque la boucle de message parvient à ce message, elle "réintroduit" cette méthode là où elle s'était arrêtée, juste après await et continue d'exécuter le reste de la méthode. Notez que ce code est à nouveau appelé à partir de la boucle de message. Par conséquent, si ce code agit longtemps sans utiliser correctement async/await, il bloquera à nouveau la boucle de message.

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 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 problèmes sous-jacents se retrouvent encore dans le code environnant, et s'ils ne le font pas, vous devez généralement déboguer une application qui se rompt de manière aléatoire sans raison apparente.


OK, alors que se passe-t-il si GetSomethingAsync lance un fil qui va se terminer en 2 secondes? Oui, alors évidemment il y a un nouveau fil en jeu. Ce fil, cependant, n'est pas car de l'async-ness de cette méthode, c'est parce que le programmeur de cette méthode a choisi un fil pour implémenter du code asynchrone. Presque toutes les E/S asynchrones n'utilisent pas de thread , elles utilisent des éléments différents. async/await par eux-mêmes ne crée pas de nouveaux threads mais bien évidemment, les "choses que nous attendons" peuvent être implémentées à l'aide de threads.

Il y a beaucoup de choses dans .NET qui ne font pas forcément tourner un fil mais qui 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 si 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 ne pas utiliser un 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 très gore.

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 DMA (accès direct à la mémoire). Il existe des processeurs spéciaux dédiés sur les cartes réseau, les cartes vidéo, les contrôleurs de disque dur, les ports série/parallèles, etc. Ces processeurs ont un accès direct au bus de mémoire et gèrent la lecture/écriture de manière totalement indépendante de la CPU. La CPU doit simplement informer le périphérique de l'emplacement en mémoire contenant les données et peut ensuite agir de manière autonome jusqu'à ce que le périphérique déclenche une interruption pour informer la CPU que la lecture/écriture est terminée.

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

83
Stephen Cleary

la seule façon pour un ordinateur de sembler faire plus d'une chose à la fois est (1) de faire plus d'une chose à la fois, (2) de le simuler en planifiant des tâches et en basculant entre elles. Donc, si async-wait ne fait ni de ceux

Ce n'est pas que wait ni parmi ceux-là. Rappelez-vous que le but de await n'est pas de rendre le code synchrone magiquement asynchrone . C'est pour activer en utilisant les mêmes techniques que nous utilisons pour écrire du code synchrone lors de l'appel en code asynchrone . Await a pour but de faire en sorte que le code qui utilise des opérations à latence élevée ressemble à un code qui utilise des opérations à latence faible . Ces opérations à latence élevée peuvent être sur des threads, sur du matériel informatique spécial, elles peuvent être en train de déchirer leur travail en petits morceaux et de le mettre dans la file d’alarme pour pouvoir être traité ultérieurement par le thread d’UI. Ils font quelque chose pour obtenir l'asynchronisme, mais ils sont ceux qui le font. Attendre vous permet simplement de tirer parti de cette asynchronisme.

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 de rap devraient sortir de ma pelouse, etc. - nous nous souvenons du monde Windows au début des années 90. 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 vous rendre . Le multitâche était coopératif . Le système d'exploitation indique à un processus qu'il doit s'exécuter et, s'il se comporte mal, il empêche tous les autres processus d'être exécutés. Il fonctionne jusqu’à ce qu’il cède et, d’une manière ou d’une autre, il doit savoir comment le récupérer là où il s’est arrêté la prochaine fois que le système d’exploitation le contrôle . Le code asynchrone à un seul thread ressemble beaucoup à cela, avec "wait" au lieu de "yield". Attendre signifie "je vais me rappeler où je suis parti 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 l'avais laissée". Je pense que vous pouvez voir comment cela rend les applications plus réactives, comme dans Windows 3 jours.

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

Il y a la clé qui vous manque. ne méthode peut revenir avant la fin de son travail. C'est l'essence même de l'asynchronisme. Une méthode retourne, elle retourne une tâche qui signifie "ce travail est en cours; dites-moi quoi faire quand il est terminé". Le travail de la méthode n'est pas terminé, même si elle est retournée .

Avant l'opérateur d'attendre, vous deviez écrire un code ressemblant à des spaghettis dans du fromage suisse pour tenir compte du fait que nous avions du travail à faire après l'achèvement, mais avec le retour et l'achèvement désynchronisés . Await vous permet d'écrire du code qui ressemble comme si le retour et l'achèvement étaient synchronisés, sans eux réellement en cours de synchronisation.

82
Eric Lippert

Je suis vraiment heureux que quelqu'un ait posé cette question, car pendant très longtemps, j'ai également pensé que les threads étaient nécessaires à la concurrence. Quand j'ai vu pour la première fois des boucles d'événement , je pensais qu'elles étaient un mensonge. Je me suis dit "il n'y a aucun moyen que ce code puisse être concurrent s'il tourne dans un seul thread". N'oubliez pas que c'est après que j'avais déjà eu du mal à comprendre la différence entre concurrence et parallélisme.

Après des recherches personnelles, j'ai finalement trouvé la pièce manquante: select() . Plus précisément, IO multiplexage, implémenté par différents noyaux sous différents noms: select(), poll(), epoll(), kqueue(). Ce sont 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 fichier à surveiller. Vous pouvez ensuite effectuer un autre appel bloqué jusqu'à ce que l'un des descripteurs de fichier surveillé change.

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

Comment cela marche-t-il? Eh bien, la réponse courte est qu’il s’agit d’une magie au niveau du noyau et du matériel. Outre le processeur, de nombreux composants se trouvent dans un ordinateur et 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 constituent le bloc de construction fondamental des boucles d'événement à thread unique, telles que node.js ou Tornado. Lorsque vous await une fonction, vous surveillez un certain événement (son achèvement), puis vous redonnez le contrôle à la boucle d'événements principale. Lorsque l'événement que vous regardez est terminé, la fonction reprend (éventuellement) là où elle s'était arrêtée. Les fonctions qui vous permettent de suspendre et de reprendre le calcul comme ceci s'appellent coroutines .

27
gardenhead

await et async utilise Tâches pas les 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 une libre déjà existante 1, thread pour appeler la méthode d’action tâche.
Créer une Task est une question de création d'un nouvel objet, bien plus rapide que de créer un nouveau thread.

Étant donné qu'une tâche est possible d'y attacher une suite , il s'agit d'un nouvel objet Task être exécuté une fois que le fil se termine.

Puisque async/await utilise Task s, ils ne créent pas de nouveaux threads .


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 à la CPU s'exécutant en parallèle (en fait, entrelacées) dans une seule CPU en utilisant aysnc/await.
Cela n’a pas pu être expliqué simplement par le fait que le système d’exploitation prend en charge la mise en file d'attente IORP.


La dernière fois que j'ai vérifié le compilateur ayant transformé les méthodes async en DFA , le travail est divisé en étapes, chacune se terminant par une instruction await.
La await commence sa Tâche et lui associe une continuation pour exécuter l'étape suivante.

Comme exemple de concept, voici un exemple de pseudo-code.
Les choses sont simplifiées pour plus de clarté et parce que je ne me souviens pas de tous les détails avec précision.

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 réalité, un pool peut avoir sa stratégie de création de tâches.

22
Margaret Bloom

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

L'utilisation de await seule ne permet pas à votre application de réagir comme par magie. Si quoi que vous fassiez dans la méthode que vous attendez des blocs de thread d'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 de manière spécifique pour qu'elle génère un nouveau thread ou utilise un port semblable à un port d'achèvement (qui renverra l'exécution dans le thread actuel et appellera autre chose pour la poursuite lorsque le port d'achèvement sera signalé). Mais cette partie est bien expliquée dans d’autres réponses.

15
Andrew Savinykh

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

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

  • traitement qui se passe sur le processeur
  • traitement qui se produit 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 utilisé (et c'est très important), le traitement sera lié à la CP, ou lié à l'IO =, et en fait, il peut être lié à une combinaison des deux.

Quelques exemples:

  • si j'utilise la méthode Write de l'objet FileStream (qui est un flux), le traitement sera, par exemple, lié à 1% de l'unité centrale et à 99% IO.
  • si j'utilise la méthode Write de l'objet NetworkStream (qui est un flux), le traitement sera, par exemple, lié à 1% de l'unité centrale et à 99% IO.
  • si j'utilise la méthode Write de l'objet Memorystream (qui est un flux), le traitement sera lié à 100% à l'UC.

Ainsi, comme vous le voyez, d’un point de vue de programmeur orienté objet, même si j’accède toujours à un objet Stream, ce qui se passe en dessous peut fortement dépendre du type ultime de l’objet.

Maintenant, pour optimiser les choses, il est parfois utile de pouvoir exécuter du code en parallèle (remarque: 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 dessert 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:

  • Fils. Il était relativement facile à utiliser, avec les classes Thread et ThreadPool. Les threads sont liés au processeur uniquement.
  • Le "vieux" début/fin/AsyncCallback modèle de programmation asynchrone. C'est juste un modèle, il ne vous dit pas si vous allez être CPU ou IO lié. Si vous examinez les classes Socket ou FileStream, c'est IO lié, ce qui est cool, mais nous l'utilisons rarement.

Async/wait est seulement 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, il s’agit "simplement" d’une enveloppe très sophistiquée comportant de nombreuses fonctionnalités.

Donc, la vraie victoire est principalement sur IO Tâches liées, tâche qui n'utilise pas le processeur, mais async/attend n'est toujours qu'un modèle de programmation, cela n'aide pas vous déterminez comment/où le traitement va se passer à 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 CPU (ce qui signifie qu'il peut être assez inutile , surtout s’il n’a pas de paramètre de jeton d’annulation), ou IO Bound (ce qui signifie que c'est probablement un doit ) , ou une combinaison des deux (puisque le modèle est assez viral, la liaison et les avantages potentiels peuvent être, à la fin, super mixés et moins évidents).

Donc, pour 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), même si j'en tirerai certainement profit avec les fichiers et les flux réseau.

13
Simon Mourier

Résumant d'autres réponses:

Async/wait est principalement créé pour les tâches liées IO, car leur utilisation permet d'éviter le blocage du thread appelant. Leur utilisation principale est avec les threads d'interface utilisateur où il n'est pas souhaitable que le thread soit bloqué sur une opération liée IO.

Async ne crée pas son propre fil. Le thread de la méthode d'appel est utilisé pour exécuter la méthode asynchrone jusqu'à ce qu'elle trouve une attente. Le même thread continue ensuite à exécuter le reste de la méthode appelante au-delà de l'appel de la méthode asynchrone. Dans la méthode asynchrone appelée, après la fin de l'attente, la poursuite peut être exécutée sur un thread du pool de threads - le seul endroit où un thread séparé entre en scène.

2
vaibhav kumar

J'essaie de l'expliquer de bas en haut. Peut-être que quelqu'un trouve cela utile. J'étais là, fait ça, réinventé, quand je faisais des jeux simples sous DOS en Pascal (bon vieux temps ...)

Alors ... Dans chaque application pilotée par les événements, il y a une boucle d'événements qui ressemble à ceci:

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

Les cadres vous cachent généralement ce détail, mais c'est là. La fonction getMessage lit le prochain événement dans la file d'attente ou attend qu'un événement se produise: déplacement de la souris, raccourci clavier, relance clavier, clic, etc. Ensuite, dispatchMessage envoie l'événement au gestionnaire d'événements approprié. Attend ensuite le prochain événement et ainsi de suite jusqu’à ce qu’un événement de fin qui quitte les boucles et termine l’application survienne.

Les gestionnaires d'événements doivent s'exécuter rapidement afin que la boucle d'événements puisse rechercher d'autres événements et que l'interface utilisateur reste sensible. Que se passe-t-il si un clic de bouton déclenche une opération coûteuse comme celle-ci?

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

L’interface utilisateur cesse de répondre jusqu’à ce que l’opération de 10 secondes se termine, le contrôle restant dans la fonction. Pour résoudre ce problème, vous devez diviser la tâche en petites parties pouvant être exécutées rapidement. Cela signifie que vous ne pouvez pas gérer le tout dans un seul événement. Vous devez effectuer une petite partie du travail, puis publier un autre événement dans la file d’événements pour demander la poursuite de celui-ci.

Donc, vous changeriez 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 est exécutée, puis un message est envoyé à la file d'attente des événements pour que l'itération suivante soit exécutée et renvoyé. Si notre exemple postFunctionCallMessage pseudo-fonction place un événement "call this function" dans la file d'attente, le répartiteur d'événements l'appellera dès qu'il l'aura atteinte. Cela permet de traiter tous les autres événements de l'interface graphique lors de l'exécution continue d'une partie d'un travail de longue durée.

Tant que cette tâche d'exécution longue est en cours d'exécution, son événement de continuation est toujours dans la file d'attente des événements. Donc, vous avez 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 réalité, 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 processeur, où le système d’exploitation a enregistré le code de changement de contexte. Vous n’avez donc pas à vous en soucier. Mais ici, vous écrivez votre propre planificateur, vous devez donc vous en préoccuper - jusqu’à présent.

Ainsi, nous pouvons exécuter de longues tâches dans un seul thread parallèle à l'interface graphique en les divisant en petits morceaux et en envoyant des événements de continuation. C'est l'idée générale de la classe Task. Il représente une pièce et lorsque vous appelez .ContinueWith, vous définissez la fonction à appeler comme pièce suivante lorsque la pièce en cours se termine (et que sa valeur de retour est transmise à la suite). La classe Task utilise un pool de threads, où il existe une boucle d'événement dans chaque thread en attente d'effectuer des tâches similaires à celles que j'ai affichées au début. De cette façon, vous pouvez avoir des millions de tâches exécutées en parallèle, mais seulement quelques threads pour les exécuter. Mais cela fonctionnerait tout aussi bien avec un seul thread - tant que vos tâches sont correctement divisées en petits morceaux, chacun d'entre eux semble s'exécuter en parallèle.

Mais faire tout ce chaînage en fractionnant manuellement le travail en petits morceaux est un travail fastidieux et gâche totalement la structure de la logique, car le code de la tâche d’arrière-plan dans son ensemble est fondamentalement un désordre .ContinueWith. C'est donc là que le compilateur vous aide. Il fait tout ce chaînage et cette continuation pour vous en arrière-plan. Quand vous dites await vous dites au compilateur que "vous arrêtez ici, ajoutez le reste de la fonction en tant que tâche continue". Le compilateur s'occupe du reste, vous n'avez donc pas à le faire.

1
Calmarius

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

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

Si l'application n'est pas bloquée, c'est parce que la machine à états peut décider de la co-routine à exécuter, à répéter, à vérifier et à décider à nouveau.

Lectures complémentaires:

Qu'est-ce que async & attend générer?

Async Await et la machine générée State

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 async awaits. Même si vous acceptez 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 et de déterminer quelle routine peut être exécutée ensemble de manière asynchrone.

0
Steve Fan