Quelle est la différence entre la programmation asynchrone et le multithreading?


234

Je pensais que c'était essentiellement la même chose - écrire des programmes qui répartissent les tâches entre les processeurs (sur les machines qui ont 2+ processeurs). Alors je lis ceci , qui dit:

Les méthodes asynchrones sont destinées à être des opérations non bloquantes. Une expression d'attente dans une méthode async ne bloque pas le thread actuel pendant l'exécution de la tâche attendue. Au lieu de cela, l'expression signe le reste de la méthode en tant que continuation et renvoie le contrôle à l'appelant de la méthode async.

Les mots clés asynchrones et en attente n'entraînent pas la création de threads supplémentaires. Les méthodes asynchrones ne nécessitent pas de multithreading car une méthode asynchrone ne s'exécute pas sur son propre thread. La méthode s'exécute sur le contexte de synchronisation actuel et utilise l'heure sur le thread uniquement lorsque la méthode est active. Vous pouvez utiliser Task.Run pour déplacer le travail lié au processeur vers un thread d'arrière-plan, mais un thread d'arrière-plan n'aide pas avec un processus qui attend simplement que les résultats soient disponibles.

et je me demande si quelqu'un peut traduire cela en anglais pour moi. Il semble faire une distinction entre l'asyncronicité (est-ce un mot?) Et le threading et impliquer que vous pouvez avoir un programme qui a des tâches asynchrones mais pas de multithreading.

Maintenant, je comprends l'idée de tâches asynchrones comme l'exemple sur pg. 467 de C # In Depth de Jon Skeet , troisième édition

async void DisplayWebsiteLength ( object sender, EventArgs e )
{
    label.Text = "Fetching ...";
    using ( HttpClient client = new HttpClient() )
    {
        Task<string> task = client.GetStringAsync("http://csharpindepth.com");
        string text = await task;
        label.Text = text.Length.ToString();
    }
}

Le asyncmot-clé signifie " Cette fonction, chaque fois qu'elle est appelée, ne sera pas appelée dans un contexte dans lequel son achèvement est requis pour tout après son appel."

En d'autres termes, l'écrire au milieu d'une tâche

int x = 5; 
DisplayWebsiteLength();
double y = Math.Pow((double)x,2000.0);

, car DisplayWebsiteLength()n'a rien à voir avec xou y, entraînera DisplayWebsiteLength()l'exécution "en arrière-plan", comme

                processor 1                |      processor 2
-------------------------------------------------------------------
int x = 5;                                 |  DisplayWebsiteLength()
double y = Math.Pow((double)x,2000.0);     |

Évidemment, c'est un exemple stupide, mais ai-je raison ou suis-je totalement confus ou quoi?

(En outre, je ne comprends pas pourquoi senderet ene sont jamais utilisés dans le corps de la fonction ci-dessus.)



senderet esuggèrent qu'il s'agit en fait d'un gestionnaire d'événements - à peu près le seul endroit où cela async voidest souhaitable. Très probablement, cela s'appelle sur un clic de bouton ou quelque chose comme ça - le résultat étant que cette action se produit de manière complètement asynchrone par rapport au reste de l'application. Mais tout est toujours sur un thread - le thread d'interface utilisateur (avec un minuscule ruban de temps sur un thread IOCP qui publie le rappel sur le thread d'interface utilisateur).
Luaan


3
Remarque très importante sur l' DisplayWebsiteLengthexemple de code: vous ne devez pas utiliser HttpClientdans une usinginstruction - sous une charge élevée, le code peut épuiser le nombre de sockets disponibles, ce qui entraîne des erreurs SocketException. Plus d'informations sur l' instanciation incorrecte .
Gan

1
@JakubLortz Je ne sais pas vraiment à qui l'article est destiné. Pas pour les débutants, car cela nécessite une bonne connaissance des threads, des interruptions, des choses liées au processeur, etc. Pas pour les utilisateurs avancés, car pour eux, tout est déjà clair. Je suis sûr que cela n'aidera personne à comprendre de quoi il s'agit - un niveau d'abstraction trop élevé.
Loreno

Réponses:


590

Votre malentendu est extrêmement courant. Beaucoup de gens apprennent que le multithreading et l'asynchronie sont la même chose, mais ils ne le sont pas.

Une analogie aide généralement. Vous cuisinez dans un restaurant. Une commande arrive pour les œufs et les toasts.

  • Synchrone: vous faites cuire les œufs, puis vous faites cuire le pain grillé.
  • Asynchrone, simple filetage: vous démarrez la cuisson des œufs et réglez une minuterie. Vous démarrez la cuisson des toasts et réglez une minuterie. Pendant qu'ils cuisinent tous les deux, vous nettoyez la cuisine. Lorsque les minuteries sonnent, retirez les œufs du feu et le pain grillé du grille-pain et servez-les.
  • Asynchrone, multithread: vous embauchez deux autres cuisiniers, un pour cuire les œufs et un pour cuire les toasts. Vous avez maintenant le problème de coordonner les cuisiniers afin qu'ils n'entrent pas en conflit les uns avec les autres dans la cuisine lors du partage des ressources. Et vous devez les payer.

Est-il maintenant logique que le multithreading ne soit qu'un type d'asynchronie? Le filetage concerne les travailleurs; l'asynchronie concerne les tâches . Dans les workflows multithreads, vous affectez des tâches aux travailleurs. Dans les workflows asynchrones à un seul thread, vous avez un graphique des tâches où certaines tâches dépendent des résultats d'autres; à mesure que chaque tâche se termine, elle invoque le code qui planifie la prochaine tâche qui peut s'exécuter, compte tenu des résultats de la tâche qui vient d'être terminée. Mais (espérons-le) vous n'avez besoin que d'un seul travailleur pour effectuer toutes les tâches, pas d'un seul travailleur par tâche.

Cela vous aidera à réaliser que de nombreuses tâches ne sont pas liées au processeur. Pour les tâches liées au processeur, il est logique d'embaucher autant de travailleurs (threads) qu'il y a de processeurs, d'affecter une tâche à chaque travailleur, d'affecter un processeur à chaque travailleur et de faire en sorte que chaque processeur ne fasse rien d'autre que de calculer le résultat comme Aussi vite que possible. Mais pour les tâches qui n'attendent pas sur un processeur, vous n'avez pas du tout besoin d'affecter un travailleur. Vous attendez simplement que le message arrive que le résultat est disponible et faites autre chose pendant que vous attendez . Lorsque ce message arrive, vous pouvez planifier la poursuite de la tâche terminée en tant que prochaine chose sur votre liste de tâches à cocher.

Examinons donc l'exemple de Jon plus en détail. Ce qui se produit?

  • Quelqu'un invoque DisplayWebSiteLength. OMS? On s'en fout.
  • Il définit une étiquette, crée un client et demande au client de récupérer quelque chose. Le client renvoie un objet représentant la tâche de récupérer quelque chose. Cette tâche est en cours.
  • Est-il en cours sur un autre fil? Probablement pas. Lisez l'article de Stephen sur pourquoi il n'y a pas de fil.
  • Nous attendons maintenant la tâche. Ce qui se produit? Nous vérifions pour voir si la tâche s'est terminée entre le moment où nous l'avons créée et nous l'avons attendue. Si oui, nous récupérons le résultat et continuons à courir. Supposons qu'il ne soit pas terminé. Nous inscrivons le reste de cette méthode comme la continuation de cette tâche et revenons .
  • Le contrôle est maintenant revenu à l'appelant. Qu'est ce que ça fait? Tout ce qu'il veut.
  • Supposons maintenant que la tâche se termine. Comment a-t-il fait ça? Peut-être qu'il s'exécutait sur un autre thread, ou peut-être que l'appelant auquel nous venons de revenir lui a permis de s'exécuter complètement sur le thread actuel. Quoi qu'il en soit, nous avons maintenant une tâche terminée.
  • La tâche terminée demande au thread correct - là encore, probablement le seul thread - d'exécuter la poursuite de la tâche.
  • Le contrôle revient immédiatement dans la méthode que nous venons de quitter au point d'attendre. Maintenant , il est un résultat disponible afin que nous puissions attribuer textet exécuter le reste de la méthode.

C'est comme dans mon analogie. Quelqu'un vous demande un document. Vous envoyez par la poste le document et continuez à faire d'autres travaux. Quand il arrive dans le courrier, vous êtes signalé et quand vous en avez envie, vous effectuez le reste du flux de travail - ouvrez l'enveloppe, payez les frais de livraison, peu importe. Vous n'avez pas besoin d'embaucher un autre travailleur pour faire tout cela pour vous.


8
@ user5648283: Le matériel n'est pas au bon niveau pour penser aux tâches. Une tâche est simplement un objet qui (1) représente qu'une valeur deviendra disponible à l'avenir et (2) peut exécuter du code (sur le thread correct) lorsque cette valeur est disponible . La façon dont une tâche individuelle obtient le résultat à l'avenir dépend de lui. Certains utiliseront du matériel spécial comme des «disques» et des «cartes réseau» pour ce faire; certains utiliseront du matériel comme des processeurs.
Eric Lippert

13
@ user5648283: Encore une fois, pensez à mon analogie. Lorsque quelqu'un vous demande de faire cuire des œufs et du pain grillé, vous utilisez du matériel spécial - une cuisinière et un grille-pain - et vous pouvez nettoyer la cuisine pendant que le matériel fait son travail. Si quelqu'un vous demande des œufs, du pain grillé et une critique originale du dernier film Hobbit, vous pouvez écrire votre critique pendant que les œufs et le pain grillé cuisent, mais vous n'avez pas besoin d'utiliser du matériel pour cela.
Eric Lippert

9
@ user5648283: Maintenant, comme pour votre question sur "réorganiser le code", considérez ceci. Supposons que vous ayez une méthode P qui a un retour de rendement et une méthode Q qui fait un foreach sur le résultat de P. Passez en revue le code. Vous verrez que nous exécutons un peu de Q puis un peu de P puis un peu de Q ... Comprenez-vous l'intérêt de cela? attendre est essentiellement le retour du rendement en tenue de soirée . Maintenant, est-ce plus clair?
Eric Lippert

10
Le grille-pain est du matériel. Le matériel n'a pas besoin d'un thread pour le réparer; disques et cartes réseau et ainsi de suite à un niveau bien inférieur à celui des threads du système d'exploitation.
Eric Lippert

5
@ShivprasadKoirala: Ce n'est absolument pas vrai du tout . Si vous croyez cela, alors vous avez de très fausses croyances à propos de l'asynchronie . L'intérêt de l'asynchronie en C # est qu'il ne crée pas de thread.
Eric Lippert

27

Javascript dans le navigateur est un excellent exemple d'un programme asynchrone qui n'a pas de threads.

Vous n'avez pas à vous soucier que plusieurs morceaux de code touchent les mêmes objets en même temps: chaque fonction finira de s'exécuter avant que tout autre javascript ne soit autorisé à s'exécuter sur la page.

Cependant, lorsque vous faites quelque chose comme une demande AJAX, aucun code n'est en cours d'exécution, donc d'autres javascript peuvent répondre à des choses comme les événements de clic jusqu'à ce que cette demande revienne et invoque le rappel qui lui est associé. Si l'un de ces autres gestionnaires d'événements est toujours en cours d'exécution lorsque la demande AJAX est renvoyée, son gestionnaire ne sera pas appelé avant d'avoir terminé. Il n'y a qu'un "thread" JavaScript en cours d'exécution, même s'il vous est possible de suspendre efficacement la chose que vous faisiez jusqu'à ce que vous ayez les informations dont vous avez besoin.

Dans les applications C #, la même chose se produit chaque fois que vous traitez avec des éléments d'interface utilisateur - vous n'êtes autorisé à interagir avec des éléments d'interface utilisateur que lorsque vous êtes sur le thread d'interface utilisateur. Si l'utilisateur cliquait sur un bouton et que vous vouliez répondre en lisant un fichier volumineux sur le disque, un programmeur inexpérimenté pourrait commettre l'erreur de lire le fichier dans le gestionnaire d'événements Click lui-même, ce qui entraînerait le gel de l'application jusqu'à ce que le le chargement du fichier est terminé, car il n'est plus autorisé à répondre aux clics, au survol ou à tout autre événement lié à l'interface utilisateur tant que ce thread n'est pas libéré.

Une option que les programmeurs peuvent utiliser pour éviter ce problème est de créer un nouveau thread pour charger le fichier, puis d'indiquer au code de ce thread que lorsque le fichier est chargé, il doit exécuter à nouveau le code restant sur le thread d'interface utilisateur afin qu'il puisse mettre à jour les éléments d'interface utilisateur en fonction de ce qu'il a trouvé dans le fichier. Jusqu'à récemment, cette approche était très populaire car c'était ce que les bibliothèques et le langage C # facilitaient, mais c'est fondamentalement plus compliqué qu'il ne doit l'être.

Si vous pensez à ce que le CPU fait quand il lit un fichier au niveau du matériel et du système d'exploitation, il émet essentiellement une instruction pour lire les données du disque dans la mémoire et pour frapper le système d'exploitation avec une "interruption" "lorsque la lecture est terminée. En d'autres termes, la lecture à partir du disque (ou de toute E / S en réalité) est une opération intrinsèquement asynchrone . Le concept d'un thread en attente de fin d'E / S est une abstraction que les développeurs de la bibliothèque ont créée pour faciliter la programmation. Ce n'est pas nécessaire.

Maintenant, la plupart des opérations d'E / S dans .NET ont une ...Async()méthode correspondante que vous pouvez invoquer, qui retourne Taskpresque immédiatement. Vous pouvez ajouter des rappels à cela Taskpour spécifier le code que vous souhaitez exécuter à la fin de l'opération asynchrone. Vous pouvez également spécifier sur quel thread vous souhaitez que ce code s'exécute et vous pouvez fournir un jeton que l'opération asynchrone peut vérifier de temps en temps pour voir si vous avez décidé d'annuler la tâche asynchrone, ce qui lui donne la possibilité d'arrêter son travail rapidement et gracieusement.

Jusqu'à ce que les async/awaitmots clés soient ajoutés, C # était beaucoup plus évident sur la façon dont le code de rappel était invoqué, car ces rappels étaient sous la forme de délégués que vous avez associés à la tâche. Afin de vous donner toujours l'avantage d'utiliser l' ...Async()opération, tout en évitant la complexité du code, async/awaitrésume la création de ces délégués. Mais ils sont toujours là dans le code compilé.

Ainsi, vous pouvez faire en sorte que votre gestionnaire d'événements d'interface utilisateur effectue awaitune opération d'E / S, libérant le thread d'interface utilisateur pour faire d'autres choses, et retournant plus ou moins automatiquement au thread d'interface utilisateur une fois que vous avez fini de lire le fichier - sans jamais avoir à le faire créer un nouveau fil.


Il n'y a qu'un "thread" JavaScript en cours d'exécution - ce n'est plus vrai avec les Web Workers .
oleksii

6
@oleksii: C'est techniquement vrai, mais je n'allais pas entrer dans les détails car l'API Web Workers elle-même est asynchrone et les Web Workers ne sont pas autorisés à avoir un impact direct sur les valeurs javascript ou le DOM sur la page Web qu'ils invoquent de, ce qui signifie que le deuxième paragraphe crucial de cette réponse reste vrai. Du point de vue du programmeur, il y a peu de différence entre invoquer un Web Worker et invoquer une requête AJAX.
StriplingWarrior
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.