Pourquoi le multithreading est-il souvent préféré pour améliorer les performances?

23

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?

user1849534
la source
2
Une façon de comparer serait de regarder le monde JavaScript, s'il n'y a pas de thread et que tout est agressivement asynchrone, en utilisant des rappels. Cela fonctionne, mais il a ses propres problèmes.
Gort the Robot
2
@StevenBurnap Comment appelez-vous les travailleurs Web?
user16764
2
"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." => Cela peut être dû à une mauvaise conception / à une utilisation inappropriée des threads. Vous rencontrez généralement ce genre de situation lorsque vos threads continuent d'échanger des données, auquel cas le multi-threading peut ne pas être la bonne réponse ou vous devrez peut-être re-partitionner les données.
assylias
1
@assylias Pour voir un ralentissement significatif dans le thread d'interface utilisateur indique une quantité excessive de verrouillage entre les threads. Soit vous avez une mauvaise mise en œuvre, soit vous essayez de marteler une cheville carrée dans un trou rond.
Evan Plaice
5
Vous dites que "les programmeurs semblent aimer la concurrence et les programmes multithread en général", j'en doute. Je dirais que "les programmeurs détestent" ... mais souvent c'est la seule chose utile à faire ...
johannes

Réponses:

34

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:

  • Le stockage (ex Amazon S3, Google Cloud Drive) est lié au processeur
  • Les serveurs Web sont liés aux E / S (Amazon EC2, Google App Engine)
  • Les bases de données sont à la fois, CPU pour les écritures / indexation et IO pour les lectures

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.

Plie d'Evan
la source
pourquoi un programmeur devrait opter pour plusieurs processus? Je veux dire que je suppose qu'avec plus d'un processus, vous avez également besoin d'une sorte de communication inter-processus qui peut ajouter une surcharge importante, est-ce quelque chose comme l'ancienne façon de faire des programmeurs Windows? quand dois-je passer au multi-processus? Merci pour votre réponse au fait, vraiment une bonne image de ce que sont asynchrones et multi-threadés.
user1849534
1
Vous supposez que la communication interprocessus augmenterait la surcharge globale. Cependant, si l'état de traitement est immuable ou ne doit gérer la synchronisation qu'au démarrage / à la fin. il peut être beaucoup plus efficace de se déployer dans des tâches plus parallèles. Le modèle d'acteur est un bon exemple, et si vous n'avez pas lu à ce sujet - cela vaut vraiment la peine d'être lu. akka.io
sylvanaar
1
@ user1849534 Plusieurs threads peuvent communiquer entre eux via la mémoire partagée + verrouillage ou IPC. Le verrouillage est plus facile mais plus difficile à déboguer si vous faites une erreur (par exemple, vous avez raté un verrou, un verrou mort). IPC est préférable si vous avez beaucoup de threads de travail car le verrouillage n'est pas bien adapté. Quoi qu'il en soit, si vous utilisez une approche multithread, il est important de maintenir la communication / synchronisation entre les threads au minimum absolu (c'est-à-dire pour minimiser les frais généraux).
Evan Plaice
1
@ akka.io Vous avez tout à fait raison. L'immuabilité est un moyen de minimiser / éliminer les frais généraux de verrouillage, mais vous encourez toujours le coût en temps du changement de contexte. Si vous souhaitez étendre la réponse pour inclure les détails sur la façon dont l'immuabilité peut résoudre les problèmes de synchronisation des threads, n'hésitez pas. Le principal point que je visais à illustrer est qu'il y a des cas où la communication asynchrone a un avantage distinct sur le processus multithread et vice versa.
Evan Plaice
(suite) Mais, honnêtement, si j'avais besoin de beaucoup de capacités de traitement liées au processeur, je sauterais le modèle d'acteur et le construirais pour évoluer vers plusieurs nœuds de réseau. La meilleure solution que j'ai vue pour cela est d'utiliser le modèle de ventilateur de tâche de 0MQ sur les communications au niveau de la prise. Voir Fig 5 @ zguide.zeromq.org/page:all .
Evan Plaice
13

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 asyncméthode est utilisée, qui awaitdevient 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, awaitsignifie 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.

Theodore Murdock
la source
+1 pour une réponse bien pensée. Je prêterais cependant attention à prendre les suggestions de Microsoft pour argent comptant. Gardez à l'esprit que .NET est une plate-forme synchrone avant tout, par conséquent l'écosystème est biaisé pour fournir de meilleures installations / documentation qui prennent en charge la création de solutions synchrones. L'inverse serait vrai pour une plate-forme asynchrone comme Node.js.
Evan Plaice
3

l'application ne répond pas et est juste lente et désagréable.

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.

Telastyn
la source
Pourquoi ? par exemple ce que vous trouvez si bananes dans la bibliothèque boost signal2?
user1849534