J'ai une question, c'est pourquoi les programmeurs semblent aimer les programmes simultanés et multi-thread en général.
J'envisage 2 approches principales ici:
- une approche asynchrone basée essentiellement sur des signaux, ou simplement une approche asynchrone comme le disent de nombreux articles et langages comme le nouveau C # 5.0 par exemple, et un "thread compagnon" qui gère la politique de votre pipeline
- une approche simultanée ou une approche multi-threading
Je vais juste dire que je pense au matériel ici et au pire des cas, et j'ai moi-même testé ces 2 paradigmes, le paradigme asynchrone est un gagnant au point que je ne comprends pas pourquoi les gens 90% du temps parler de multi-threading quand ils veulent accélérer les choses ou faire un bon usage de leurs ressources.
J'ai testé des programmes multi-threads et un programme asynchrone sur une vieille machine avec un quad-core Intel qui n'offre pas de contrôleur de mémoire à l'intérieur du CPU, la mémoire est entièrement gérée par la carte mère, bien dans ce cas les performances sont horribles avec un application multi-thread, même un nombre relativement faible de threads comme 3-4-5 peut être un problème, l'application ne répond pas et est juste lente et désagréable.
Une bonne approche asynchrone, en revanche, n'est probablement pas plus rapide mais ce n'est pas pire non plus, mon application attend juste le résultat et ne se bloque pas, elle est réactive et il y a une bien meilleure mise à l'échelle en cours.
J'ai également découvert qu'un changement de contexte dans le monde des threads n'est pas si bon marché dans le scénario du monde réel, il est en fait assez cher, surtout lorsque vous avez plus de 2 threads qui doivent faire un cycle et s'échanger entre eux pour être calculés.
Sur les processeurs modernes, la situation n'est pas vraiment différente, le contrôleur de mémoire est intégré, mais mon point est qu'un processeur x86 est essentiellement une machine série et le contrôleur de mémoire fonctionne de la même manière qu'avec l'ancienne machine avec un contrôleur de mémoire externe sur la carte mère . Le changement de contexte est toujours un coût pertinent dans mon application et le fait que le contrôleur de mémoire soit intégré ou que le processeur plus récent ait plus de 2 cœurs n'est pas une bonne affaire pour moi.
Pour ce que j'ai vécu, l'approche simultanée est bonne en théorie mais pas si bonne en pratique, avec le modèle de mémoire imposé par le matériel, il est difficile de faire un bon usage de ce paradigme, cela introduit également beaucoup de problèmes allant de l'utilisation de mes structures de données à la jonction de plusieurs threads.
De plus, les deux paradigmes n'offrent aucune sécurité lorsque la tâche ou le travail sera effectué à un certain moment, ce qui les rend vraiment similaires d'un point de vue fonctionnel.
Selon le modèle de mémoire X86, pourquoi la majorité des gens suggèrent d'utiliser la concurrence avec C ++ et pas seulement une approche asynchrone? Aussi pourquoi ne pas considérer le pire des cas d'un ordinateur où le changement de contexte est probablement plus cher que le calcul lui-même?
la source
Réponses:
Vous avez plusieurs cœurs / processeurs, utilisez- les
Async est préférable pour effectuer un traitement lié aux E / S lourd, mais qu'en est-il du traitement lié au processeur lourd?
Le problème se pose lorsque des blocs de code à thread unique (c'est-à-dire se bloquent) sur un processus de longue durée. Par exemple, rappelez-vous que lorsque l'impression d'un document de traitement de texte entraînerait le gel de l'ensemble de l'application jusqu'à l'envoi du travail? Le gel des applications est un effet secondaire du blocage d'une application à un seul thread pendant une tâche gourmande en ressources processeur.
Dans une application multithread, les tâches gourmandes en CPU (ex. Un travail d'impression) peuvent être envoyées à un thread de travail d'arrière-plan, libérant ainsi le thread d'interface utilisateur.
De même, dans une application multi-processus, le travail peut être envoyé via la messagerie (ex IPC, sockets, etc.) à un sous-processus spécialement conçu pour traiter les travaux.
En pratique, le code asynchrone et multi-thread / processus ont chacun leurs avantages et leurs inconvénients.
Vous pouvez voir la tendance dans les principales plates-formes cloud, car elles offriront des instances spécialisées pour le traitement lié au processeur et des instances spécialisées pour le traitement lié aux E / S.
Exemples:
Pour le mettre en perspective ...
Un serveur Web est un parfait exemple d'une plate-forme fortement liée aux E / S. Un serveur Web multithread qui attribue un thread par connexion ne s'adapte pas bien car chaque thread engendre plus de surcharge en raison de la quantité accrue de changement de contexte et de verrouillage de thread sur les ressources partagées. Alors qu'un serveur Web asynchrone utiliserait un seul espace d'adressage.
De même, une application spécialisée pour l'encodage vidéo fonctionnerait beaucoup mieux dans un environnement multi-thread car le traitement lourd impliqué bloquerait le thread principal jusqu'à ce que le travail soit terminé. Il existe des moyens d'atténuer cela, mais il est beaucoup plus facile d'avoir un seul thread gérant une file d'attente, un deuxième thread gérant le nettoyage et un pool de threads gérant le traitement lourd. La communication entre les threads ne se produit que lorsque les tâches sont affectées / terminées, de sorte que la surcharge de verrouillage des threads est réduite au strict minimum.
La meilleure application utilise souvent une combinaison des deux. Une application Web, par exemple, peut utiliser nginx (c'est-à-dire asynchrone à un seul thread) comme équilibreur de charge pour gérer le torrent de requêtes entrantes, un serveur web asynchrone similaire (ex Node.js) pour gérer les requêtes http et un ensemble de serveurs à plusieurs threads gérer le téléchargement / streaming / encodage de contenu, etc ...
Il y a eu beaucoup de guerres de religion au fil des ans entre les modèles multi-threads, multi-processus et asynchrones. Comme pour la plupart des choses, la meilleure réponse devrait être "cela dépend".
Il suit la même ligne de pensée qui justifie l'utilisation des architectures GPU et CPU en parallèle. Deux systèmes spécialisés fonctionnant de concert peuvent avoir une bien meilleure amélioration qu'une approche monolithique unique.
Ni l'un ni l'autre ne sont meilleurs parce que les deux ont leur utilité. Utilisez le meilleur outil pour le travail.
Mise à jour:
J'ai supprimé la référence à Apache et apporté une correction mineure. Apache utilise un modèle multiprocessus qui opère un processus pour chaque requête augmentant la quantité de changement de contexte au niveau du noyau. De plus, comme la mémoire ne peut pas être partagée entre les processus, chaque demande entraîne un coût de mémoire supplémentaire.
Le multi-threading se déplace nécessitant de la mémoire supplémentaire car il repose sur une mémoire partagée entre les threads. La mémoire partagée supprime la surcharge de mémoire supplémentaire mais encourt toujours la pénalité d'un changement de contexte accru. De plus - pour garantir que les conditions de concurrence ne se produisent pas - des verrous de threads (qui garantissent un accès exclusif à un seul thread à la fois) sont requis pour toutes les ressources partagées entre les threads.
C'est drôle que vous disiez, "les programmeurs semblent aimer la simultanéité et les programmes multithread en général." La programmation multithread est universellement redoutée par quiconque en a fait une grande partie en son temps. Les verrous morts (un bug qui se produit lorsqu'une ressource est verrouillée par erreur par deux sources différentes bloquant à la fois la fin) et les conditions de concurrence (où le programme produira par erreur le mauvais résultat au hasard en raison d'un séquencement incorrect) sont parmi les plus difficiles à suivre vers le bas et fixer.
Update2:
Contrairement à la déclaration générale selon laquelle IPC est plus rapide que les communications réseau (c'est-à-dire socket). Ce n'est pas toujours le cas . Gardez à l'esprit qu'il s'agit de généralisations et que les détails spécifiques à l'implémentation peuvent avoir un impact énorme sur le résultat.
la source
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, quiawait
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.
la source
Et il y a votre problème. Une interface utilisateur réactive ne fait pas une application performante. Souvent le contraire. Un tas de temps est passé à vérifier les entrées de l'interface utilisateur plutôt que de laisser les threads de travail faire leur travail.
En ce qui concerne «juste» une approche asynchrone, il s'agit également du multithreading, bien que modifié pour ce cas d'utilisation particulier dans la plupart des environnements . Dans d'autres, cette async se fait via des coroutines qui ne sont pas toujours concurrentes.
Franchement, je trouve que les opérations asynchrones sont plus difficiles à raisonner et à utiliser d'une manière qui offre réellement des avantages (performances, robustesse, maintenabilité) même par rapport à ... des approches plus manuelles.
la source