web-dev-qa-db-fra.com

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

Je pensais que c’était fondamentalement la même chose: écrire des programmes qui répartissaient les tâches entre les processeurs (sur les machines équipées de 2 processeurs ou plus). Puis je lis https://msdn.Microsoft.com/en-us/library/hh191443.aspx , qui dit

Les méthodes asynchrones sont conçues pour être des opérations non bloquantes. Une expression en attente dans une méthode asynchrone ne bloque pas le thread en cours pendant l'exécution de la tâche attendue. Au lieu de cela, l'expression inscrit le reste de la méthode en tant que continuation et redonne le contrôle à l'appelant de la méthode asynchrone.

Les mots-clés async et wait 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é à la CPU vers un fil d’arrière-plan, mais un fil d’arrière-plan ne vous aide pas dans 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 asyncronisme (est-ce un mot?) Et threading et implique 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 telles que l’exemple de la page. 467 de la troisième édition de Cs In Depth de Jon Skeet

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 mot clé async signifie ". Cette fonction, à chaque appel, ne sera pas appelée dans un contexte dans lequel elle doit être complétée pour que tout ce qui se passera après son appel soit 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);

, _ étant donné que DisplayWebsiteLength() n'a rien à voir avec x ou y, DisplayWebsiteLength() sera exécuté "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 sender et e ne sont jamais utilisés dans le corps de la fonction ci-dessus.)

155
user5648283

Votre malentendu est extrêmement commun. Beaucoup de gens apprennent que le multithreading et l'asynchronisme sont la même chose, mais ce n'est pas le cas.

Une analogie aide généralement. Vous cuisinez dans un restaurant. Une commande vient pour des oeufs et des toasts.

  • Synchrone: vous faites cuire les œufs, puis vous faites cuire le pain grillé.
  • Asynchrone, à filetage unique: vous démarrez la cuisson des œufs et définissez une minuterie. Vous commencez la cuisson du pain grillé et définissez une minuterie. Pendant qu'ils cuisinent, 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 la cuisson des œufs et un pour la cuisson du pain grillé. Vous avez maintenant le problème de coordonner les cuisiniers de manière à ce qu’ils n’entrent pas en conflit dans la cuisine lors du partage des ressources. Et vous devez les payer.

Maintenant, est-il logique que le multithreading ne soit qu'un type d'asynchronisme? Le filetage concerne les travailleurs; l'asynchronie concerne les tâches . Dans les flux de travail multithreads, vous affectez des tâches à des travailleurs. Dans les workflows mono-threadés asynchrones, vous avez un graphique des tâches dans lesquelles certaines tâches dépendent des résultats d'autres tâches. à la fin de chaque tâche, elle appelle le code qui planifie la prochaine tâche pouvant être exécutée, en fonction 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, et non d’un travailleur par tâche.

Il sera utile de réaliser que de nombreuses tâches ne sont pas liées au processeur. Pour les tâches liées au processeur, il est judicieux d’embaucher autant de travailleurs (threads) qu’il ya de processeurs, d’attribuer une tâche à chaque travailleur, d’attribuer un processeur à chaque travailleur et de laisser chaque processeur s’acquitter de sa tâche autrement qu'en calculant le résultat. Aussi vite que possible. Mais pour les tâches qui n'attendent pas un processeur, vous n'avez pas du tout besoin d'affecter un ouvrier. Vous attendez simplement le message indiquant que le résultat est disponible et faites autre chose en attendant . Lorsque ce message arrive, vous pouvez planifier la poursuite de la tâche terminée en tant que tâche suivante de votre liste de tâches à cocher.

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

  • Quelqu'un appelle DisplayWebSiteLength. Qui? On s'en fiche.
  • 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 d'extraction. Cette tâche est en cours.
  • Est-ce en cours sur un autre thread? Probablement pas. Lire article de Stephen sur pourquoi il n'y a pas de fil.
  • Maintenant nous attendons la tâche. Ce qui se produit? Nous vérifions si la tâche est terminée entre le moment où nous l'avons créée et celle que nous avons attendue. Si oui, alors 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 suite de cette tâche et retournons .
  • Maintenant le contrôle est retourné à l'appelant. Qu'est ce que ça fait? Tout ce qu'il veut.
  • Supposons maintenant que la tâche est terminée. 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 renvoyer l'a autorisé à 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 bon thread (à nouveau probablement le thread uniquement ) pour exécuter la suite de la tâche.
  • Le contrôle revient immédiatement dans la méthode que nous venons de laisser au point d'attendre. Maintenant est un résultat disponible afin que nous puissions affecter text et exécuter le reste de la méthode.

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

413
Eric Lippert

Le navigateur Javascript est un bon exemple de programme asynchrone sans thread.

Vous n'avez pas à vous préoccuper du fait que plusieurs morceaux de code touchent les mêmes objets en même temps: chaque fonction s'achèvera avant que tout autre javascript soit autorisé à s'exécuter sur la page.

Toutefois, lorsqu’il s’agit d’une requête comme une requête AJAX, aucun code n’est en cours d’exécution, ce qui permet à d’autres javascript de réagir à des événements tels que les événements de clic jusqu’à ce que cette requête 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 récupérée, son gestionnaire ne sera pas appelé jusqu'à ce qu'ils aient terminé. Un seul "fil" JavaScript est en cours d'exécution, même s'il est possible de mettre en pause ce que vous faisiez jusqu'à ce que vous obteniez les informations dont vous avez besoin.

Dans les applications C #, la même chose se produit chaque fois que vous utilisez des éléments d'interface utilisateur: vous n'êtes autorisé à interagir avec des éléments d'interface utilisateur que si vous êtes sur le thread d'interface utilisateur. Si l'utilisateur clique sur un bouton et que vous souhaitez répondre en lisant un fichier volumineux sur le disque, un programmeur inexpérimenté peut commettre l'erreur de lire le fichier dans le gestionnaire d'événements click lui-même, ce qui entraînerait le "blocage" de l'application jusqu'au Le chargement du fichier est terminé car il n'est pas autorisé à répondre aux clics, survols ou autres événements liés à l'interface utilisateur tant que ce thread n'est pas libéré.

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

Si vous pensez à ce que fait le processeur lorsqu'il lit un fichier au niveau du matériel/du système d'exploitation, il émet en gros une instruction permettant de lire des données du disque dans la mémoire et de frapper le système d'exploitation avec une "interruption" lorsque la lecture est terminée. En d’autres termes, lire sur le disque (ou n’importe quelle E/S) est une opération intrinsèquement asynchrone. Le concept d'un thread en attente d'exécution de cette E/S est une abstraction que les développeurs de bibliothèques 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 méthode correspondante que vous pouvez appeler, ...Async(), qui renvoie presque immédiatement une Task. Vous pouvez ajouter des rappels à cette Task pour spécifier le code que vous souhaitez exécuter à la fin de l'opération asynchrone. Vous pouvez également spécifier le thread sur lequel vous souhaitez exécuter ce code et vous pouvez fournir un jeton que l'opération asynchrone peut vérifier de temps à autre pour voir si vous avez décidé d'annuler la tâche asynchrone, ce qui lui donne la possibilité d'arrêter rapidement son travail. et gracieusement.

Jusqu'à l'ajout des mots-clés async/await, C # expliquait beaucoup plus clairement comment le code de rappel était appelé, car ces rappels se présentaient sous la forme de délégués que vous avez associés à la tâche. Afin de toujours vous faire bénéficier de l'utilisation de l'opération ...Async(), tout en évitant la complexité du code, async/await détourne la création de ces délégués. Mais ils sont toujours là dans le code compilé.

Ainsi, vous pouvez avoir votre gestionnaire d’événements UI await une opération d’E/S, ce qui libère le thread d’UI et permet de revenir plus ou moins automatiquement au thread d’UI une fois la lecture du fichier terminée. -sans jamais avoir à créer un nouveau fil.

19
StriplingWarrior