L' approche asynchrone de Microsoft est un bon substitut aux objectifs les plus courants de la programmation multithread: améliorer la réactivité par rapport aux tâches d'E / S.
Cependant, il est important de réaliser que l'approche asynchrone n'est pas du tout capable d'améliorer les performances ou d'améliorer la réactivité en ce qui concerne les tâches gourmandes en CPU.
Multithreading pour la réactivité
Le multithreading pour la réactivité est le moyen traditionnel de maintenir un programme réactif pendant les tâches d'E / S lourdes ou les tâches de calcul lourdes. Vous enregistrez des fichiers sur un thread d'arrière-plan, afin que l'utilisateur puisse continuer son travail, sans avoir à attendre que le disque dur termine sa tâche. Le thread IO bloque souvent l'attente de la fin d'une partie de l'écriture, les changements de contexte sont donc fréquents.
De même, lorsque vous effectuez un calcul complexe, vous souhaitez autoriser un changement de contexte régulier afin que l'interface utilisateur puisse rester réactive et que l'utilisateur ne pense pas que le programme s'est écrasé.
Le but ici n'est pas, en général, de faire fonctionner plusieurs threads sur différents CPU. Au lieu de cela, nous souhaitons simplement que des changements de contexte se produisent entre la tâche d'arrière-plan de longue durée et l'interface utilisateur, afin que l'interface utilisateur puisse mettre à jour et répondre à l'utilisateur pendant l'exécution de la tâche d'arrière-plan. En général, l'interface utilisateur ne prendra pas beaucoup de puissance CPU, et le framework de thread ou le système d'exploitation décidera généralement de les exécuter sur le même CPU.
Nous perdons en fait les performances globales en raison du coût supplémentaire du changement de contexte, mais nous ne nous en soucions pas car les performances du processeur n'étaient pas notre objectif. Nous savons que nous avons généralement plus de puissance CPU que nous n'en avons besoin, et donc notre objectif en ce qui concerne le multithreading est de faire une tâche pour l'utilisateur sans perdre son temps.
L'alternative "asynchrone"
L '"approche asynchrone" change cette image en activant les changements de contexte dans un seul thread. Cela garantit que toutes nos tâches s'exécuteront sur un seul processeur, et peut apporter quelques améliorations de performances modestes en termes de moins de création / nettoyage de threads et moins de changements de contexte réel entre les threads.
Au lieu de créer un nouveau thread pour attendre la réception d'une ressource réseau (par exemple le téléchargement d'une image), une async
méthode est utilisée, qui await
devient l'image disponible et, dans l'intervalle, cède la place à la méthode appelante.
Le principal avantage ici est que vous n'avez pas à vous soucier des problèmes de threads comme éviter les blocages, car vous n'utilisez pas du tout de verrous et de synchronisation, et il y a un peu moins de travail pour le programmeur qui configure le thread d'arrière-plan et revient. sur le thread d'interface utilisateur lorsque le résultat revient afin de mettre à jour l'interface utilisateur en toute sécurité.
Je n'ai pas trop approfondi les détails techniques, mais mon impression est que la gestion du téléchargement avec une activité CPU légère occasionnelle devient une tâche non pas pour un thread séparé, mais plutôt quelque chose de plus comme une tâche dans la file d'attente d'événements de l'interface utilisateur, et lorsque le le téléchargement est terminé, la méthode asynchrone reprend à partir de cette file d'attente d'événements. En d'autres termes, await
signifie quelque chose qui s'apparente à "vérifier si le résultat dont j'ai besoin est disponible, sinon, me remettre dans la file d'attente des tâches de ce thread".
Notez que cette approche ne résoudrait pas le problème d'une tâche gourmande en CPU: il n'y a pas de données à attendre, donc nous ne pouvons pas obtenir les changements de contexte dont nous avons besoin sans créer un véritable thread de travail en arrière-plan. Bien sûr, il peut toujours être pratique d'utiliser une méthode asynchrone pour démarrer le thread d'arrière-plan et renvoyer le résultat, dans un programme qui utilise de manière omniprésente l'approche asynchrone.
Multithreading pour la performance
Puisque vous parlez de «performances», j'aimerais également discuter de la façon dont le multithreading peut être utilisé pour des gains de performances, ce qui est tout à fait impossible avec l'approche asynchrone à un seul thread.
Lorsque vous êtes réellement dans une situation où vous n'avez pas assez de puissance CPU sur un seul CPU et que vous souhaitez utiliser le multithreading pour des performances, c'est souvent difficile à faire. D'un autre côté, si un processeur ne dispose pas d'une puissance de traitement suffisante, c'est aussi souvent la seule solution qui pourrait permettre à votre programme de faire ce que vous souhaitez accomplir dans un délai raisonnable, ce qui rend le travail intéressant.
Parallélisme trivial
Bien sûr, il peut parfois être facile d'obtenir une accélération réelle du multithreading.
Si vous avez un grand nombre de tâches indépendantes à forte intensité de calcul (c'est-à-dire des tâches dont les données d'entrée et de sortie sont très petites par rapport aux calculs qui doivent être effectués pour déterminer le résultat), vous pouvez souvent obtenir une accélération significative en créer un pool de threads (dimensionnés de manière appropriée en fonction du nombre de processeurs disponibles) et avoir un thread principal pour distribuer le travail et collecter les résultats.
Multithreading pratique pour la performance
Je ne veux pas me présenter comme trop expert, mais mon impression est que, en général, le multithreading le plus pratique pour les performances qui se produit de nos jours cherche des endroits dans une application qui ont un parallélisme trivial et utilisent plusieurs threads pour récolter les fruits.
Comme pour toute optimisation, il est généralement préférable d'optimiser après avoir profilé les performances de votre programme et identifié les points chauds: il est facile de ralentir un programme en décidant arbitrairement que cette partie doit s'exécuter dans un thread et cette partie dans un autre, sans déterminer d'abord si les deux parties prennent une partie importante du temps CPU.
Un thread supplémentaire signifie plus de coûts de configuration / démontage et plus de changements de contexte ou plus de coûts de communication inter-CPU. S'il ne fait pas assez de travail pour compenser ces coûts s'il est sur un processeur séparé et n'a pas besoin d'être un thread séparé pour des raisons de réactivité, cela ralentira les choses sans aucun avantage.
Recherchez les tâches qui ont peu d'interdépendances et qui occupent une partie importante de l'exécution de votre programme.
S'ils n'ont pas d'interdépendances, alors c'est un cas de parallélisme trivial, vous pouvez facilement configurer chacun avec un fil et profiter des avantages.
Si vous pouvez trouver des tâches avec une interdépendance limitée, de sorte que le verrouillage et la synchronisation pour échanger des informations ne les ralentissent pas de manière significative, alors le multithreading peut donner une certaine accélération, à condition que vous preniez soin d'éviter les dangers de blocage dus à une logique défectueuse lors de la synchronisation ou de la synchronisation. résultats incorrects en raison de la non synchronisation lorsque cela est nécessaire.
Alternativement, certaines des applications les plus courantes pour le multithreading ne recherchent pas (dans un sens) l'accélération d'un algorithme prédéterminé, mais plutôt un budget plus important pour l'algorithme qu'ils envisagent d'écrire: si vous écrivez un moteur de jeu , et votre IA doit prendre une décision à l'intérieur de votre fréquence d'images, vous pouvez souvent donner à votre IA un budget de cycle de CPU plus important si vous pouvez lui donner son propre CPU.
Cependant, assurez-vous de profiler les threads et assurez-vous qu'ils font suffisamment de travail pour compenser le coût à un moment donné.
Algorithmes parallèles
Il existe également de nombreux problèmes qui peuvent être accélérés à l'aide de plusieurs processeurs, mais qui sont trop monolithiques pour être simplement répartis entre les processeurs.
Les algorithmes parallèles doivent être soigneusement analysés pour leurs temps d'exécution big-O par rapport au meilleur algorithme non parallèle disponible, car il est très facile pour le coût de communication inter-CPU d'éliminer les avantages de l'utilisation de plusieurs CPU. En général, ils doivent utiliser moins de communication inter-CPU (en termes big-O) qu'ils n'utilisent de calculs sur chaque CPU.
Pour le moment, c'est encore en grande partie un espace pour la recherche universitaire, en partie à cause de l'analyse complexe requise, en partie parce que le parallélisme trivial est assez courant, en partie parce que nous n'avons pas encore autant de cœurs de processeur sur nos ordinateurs que des problèmes qui ne peut pas être résolu dans un délai raisonnable sur un processeur pourrait être résolu dans un délai raisonnable en utilisant tous nos processeurs.