Alors que les threads peuvent accélérer l'exécution du code, sont-ils réellement nécessaires? Est-ce que chaque morceau de code peut être fait en utilisant un seul thread ou existe-t-il quelque chose qui ne peut être accompli qu'en utilisant plusieurs threads?
Tout d'abord, les threads ne peuvent pas accélérer l'exécution du code. Ils ne font pas fonctionner l'ordinateur plus rapidement. Tout ce qu'ils peuvent faire, c'est augmenter l'efficacité de l'ordinateur en utilisant du temps qui serait autrement perdu. Dans certains types de traitement, cette optimisation peut augmenter l'efficacité et réduire le temps de fonctionnement.
La réponse simple est oui. Vous pouvez écrire n'importe quel code à exécuter sur un seul thread. Preuve: un système à processeur unique ne peut exécuter les instructions que de manière linéaire. Le fait d'avoir plusieurs lignes d'exécution est effectué par le système d'exploitation, traitant les interruptions, enregistrant l'état du thread actuel et en démarrant un autre.
La réponse complexe est ... plus complexe! La raison pour laquelle les programmes multithreads peuvent souvent être plus efficaces que les programmes linéaires est due à un "problème" matériel. Le CPU peut exécuter des calculs plus rapidement que la mémoire et le stockage dur IO. Ainsi, une instruction "add", par exemple, s'exécute beaucoup plus rapidement qu'une "fetch". Les caches et la récupération d'instructions de programme dédiées (pas sûr du terme exact ici) peuvent lutter contre cela dans une certaine mesure, mais le problème de vitesse reste.
Le thread est un moyen de lutter contre ce décalage en utilisant le CPU pour les instructions liées au CPU pendant que les instructions IO sont terminées. Un plan d'exécution de thread typique serait probablement: récupérer des données, traiter des données, écrire des données. que la récupération et l'écriture prennent 3 cycles et le traitement en prend un, à des fins d'illustration. Vous pouvez voir que pendant que l'ordinateur lit ou écrit, il fait rien pour 2 cycles chacun? De toute évidence, il est paresseux, et nous devons casser notre fouet d'optimisation!
Nous pouvons réécrire le processus en utilisant le filetage pour utiliser ce temps perdu:
Etc. Évidemment, c'est un exemple quelque peu artificiel, mais vous pouvez voir comment cette technique peut utiliser le temps qui serait autrement passé à attendre les E/S.
Notez que le threading comme indiqué ci-dessus ne peut augmenter l'efficacité que sur les processus fortement liés IO. Si un programme calcule principalement des choses, il n'y aura pas beaucoup de "trous" dans lesquels nous pourrions faire plus de travail) . En outre, il existe un surcoût de plusieurs instructions lors du basculement entre les threads. Si vous exécutez trop de threads, le processeur passera la majeure partie de son temps à changer et ne travaillera pas vraiment sur le problème. Cela s'appelle thrashing =.
C'est bien beau pour un processeur monocœur, mais la plupart des processeurs modernes ont deux cœurs ou plus. Les threads poursuivent toujours le même objectif - maximiser l'utilisation du processeur, mais cette fois nous avons la possibilité d'exécuter deux instructions distinctes en même temps . This can diminue le temps d'exécution est multiplié par le nombre de cœurs disponibles, car l'ordinateur est en fait multitâche, pas de changement de contexte.
Avec plusieurs cœurs, les threads fournissent une méthode de répartition du travail entre les deux cœurs. Ce qui précède s'applique toujours à chaque noyau individuel; Un programme qui exécute une efficacité maximale avec deux threads sur un cœur fonctionnera très probablement avec une efficacité maximale avec environ quatre threads sur deux cœurs. (L'efficacité est mesurée ici par le nombre minimal d'exécutions d'instructions NOP.)
Les problèmes liés à l'exécution de threads sur plusieurs cœurs (par opposition à un seul cœur) sont généralement résolus par le matériel. Le CPU s'assurera qu'il verrouille les emplacements de mémoire appropriés avant de le lire/écrire dessus. (J'ai lu qu'il utilise un bit de drapeau spécial en mémoire pour cela, mais cela pourrait être accompli de plusieurs manières.) En tant que programmeur avec des langages de niveau supérieur, vous n'avez pas à vous soucier de plus sur deux cœurs lorsque vous devrait en avoir un.
TL; DR: Les threads peuvent fractionner le travail pour permettre à l'ordinateur de traiter plusieurs tâches de manière asynchrone. Cela permet à l'ordinateur de fonctionner avec une efficacité maximale en utilisant tout le temps de traitement disponible, plutôt que de se verrouiller lorsqu'un processus attend une ressource.
Que peuvent faire plusieurs threads qu'un seul thread ne peut pas faire?
Rien.
Croquis de preuve simple:
Notez, cependant, qu'il y a une grande hypothèse cachée là-dedans: à savoir que la langue utilisée dans le thread unique est Turing-complete.
Ainsi, la question la plus intéressante serait: "L'ajout de juste multi-threading à un langage non-Turing-complet le rend-il Turing-complet?" Et je crois que la réponse est "oui".
Prenons le total des langages fonctionnels. [Pour ceux qui ne sont pas familiers: tout comme la programmation fonctionnelle est la programmation avec des fonctions, la programmation fonctionnelle totale est la programmation avec les fonctions totales.]
Les langages fonctionnels totaux ne sont évidemment pas complets de Turing: vous ne pouvez pas écrire une boucle infinie dans un TFPL (en fait, c'est à peu près la définition de "total"), mais vous pouvez = dans une machine de Turing, ergo il existe au moins un programme qui ne peut pas être écrit dans un TFPL mais peut l'être dans un UTM, donc les TFPL sont moins puissants en termes de calcul que les UTM.
Cependant, dès que vous ajoutez un thread à un TFPL, vous obtenez des boucles infinies: faites simplement chaque itération de la boucle dans un nouveau thread. Chaque thread individuel renvoie toujours un résultat, c'est donc Total, mais chaque thread génère également un nouvea thread qui exécute l'itération suivante, à l'infini.
Je pense que cette langue serait Turing-complete.
À tout le moins, il répond à la question d'origine:
Que peuvent faire plusieurs threads qu'un seul thread ne peut pas faire?
Si vous avez un langage qui ne peut pas faire de boucles infinies, alors le multi-threading vous permet de faire des boucles infinies.
Notez bien sûr que la génération d'un thread est un effet secondaire et donc notre langage étendu n'est plus seulement Total, il n'est même plus fonctionnel.
En théorie, tout ce qu'un programme multithread fait peut également être fait avec un programme à un seul thread, mais plus lentement.
En pratique, la différence de vitesse peut être si importante qu'il est impossible d'utiliser un programme à un seul thread pour la tâche. Par exemple. si vous avez un travail de traitement de données par lots en cours d'exécution tous les soirs et qu'il faut plus de 24 heures pour terminer sur un seul thread, vous n'avez pas d'autre option que de le rendre multithread. (En pratique, le seuil est probablement encore moins: souvent, ces tâches de mise à jour doivent se terminer tôt le matin, avant que les utilisateurs ne recommencent à utiliser le système. En outre, d'autres tâches peuvent en dépendre, qui doivent également se terminer pendant la même nuit. le temps d'exécution disponible peut être aussi faible que quelques heures/minutes.)
Le travail informatique sur plusieurs threads est une forme de traitement distribué; vous distribuez le travail sur plusieurs threads. Un autre exemple de traitement distribué (utilisant plusieurs ordinateurs au lieu de plusieurs threads) est l'économiseur d'écran SETI: croquer autant de données de mesure sur un seul processeur prendrait énormément de temps et les chercheurs préféreraient voir les résultats avant la retraite ;-) Cependant, ils n'ont pas le budget pour louer un supercalculateur aussi longtemps, alors ils répartissent le travail sur des millions d'ordinateurs domestiques, pour le rendre bon marché.
Bien que les threads semblent être une petite étape du calcul séquentiel, en fait, ils représentent une étape énorme. Ils rejettent les propriétés les plus essentielles et les plus attrayantes du calcul séquentiel: la compréhensibilité, la prévisibilité et le déterminisme. Les threads, en tant que modèle de calcul, sont extrêmement non déterministes, et le travail du programmeur devient celui d'élaguer ce non-déterminisme.
- Le problème avec les threads (www.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-1.pdf).
Bien qu'il existe certains avantages de performances qui peuvent être obtenus en utilisant des threads dans la mesure où vous pouvez répartir le travail sur plusieurs cœurs, ils ont souvent un prix très intéressant.
L'un des inconvénients de l'utilisation de threads non mentionnés ici est la perte de compartimentation des ressources que vous obtenez avec les espaces de processus à thread unique. Par exemple, disons que vous rencontrez le cas d'une erreur de segmentation. Dans certains cas, il est possible de s'en remettre dans une application multi-processus en laissant simplement l'enfant défaillant mourir et en réapparaître une nouvelle. C'est le cas dans le backend préfork d'Apache. Lorsqu'une instance httpd monte en flèche, le pire des cas est que la demande HTTP particulière peut être supprimée pour ce processus, mais Apache génère un nouvel enfant et souvent la demande si elle est simplement renvoyée et traitée. Le résultat final est qu'Apache dans son ensemble n'est pas supprimé avec le thread défectueux.
Une autre considération dans ce scénario est les fuites de mémoire. Il y a des cas où vous pouvez gérer gracieusement un plantage de thread (sous UNIX, la récupération à partir de certains signaux spécifiques - même segfault/fpviolation - est possible), mais même dans ce cas, vous pouvez avoir divulgué toute la mémoire allouée par ce thread (malloc, nouveau, etc.). Ainsi, bien que votre processus puisse continuer à vivre, il perd de plus en plus de mémoire au fil du temps à chaque panne/récupération. Encore une fois, il existe dans une certaine mesure des moyens de minimiser cela, comme l'utilisation par Apache des pools de mémoire. Mais cela ne protège toujours pas contre la mémoire qui pourrait avoir été allouée par des bibliothèques tierces que le thread a pu utiliser.
Et, comme certaines personnes l'ont souligné, comprendre les primitives de synchronisation est peut-être la chose la plus difficile à bien faire. Ce problème en lui-même - obtenir la bonne logique générale pour tout votre code - peut être un énorme casse-tête. Des blocages mystérieux sont susceptibles de se produire aux moments les plus étranges, et parfois même jusqu'à ce que votre programme soit en cours de production, ce qui rend le débogage encore plus difficile. Ajoutez à cela le fait que les primitives de synchronisation varient souvent considérablement selon la plate-forme (Windows vs POSIX), et le débogage peut souvent être plus difficile, ainsi que la possibilité de conditions de concurrence à tout moment (démarrage/initialisation, runtime et arrêt), la programmation avec des threads n'a vraiment aucune pitié pour les débutants. Et même pour les experts, il y a encore peu de pitié simplement parce que la connaissance du filetage lui-même ne minimise pas la complexité en général. Chaque ligne de code threadé semble parfois aggraver de manière exponentielle la complexité globale du programme et augmenter la probabilité d'un blocage caché ou d'une condition de concurrence étrange à la surface à tout moment. Il peut également être très difficile d'écrire des cas de test pour dénicher ces choses.
C'est pourquoi certains projets comme Apache et PostgreSQL sont pour la plupart basés sur des processus. PostgreSQL exécute chaque thread principal dans un processus distinct. Bien sûr, cela ne résout toujours pas le problème de la synchronisation et des conditions de concurrence, mais cela ajoute beaucoup de protection et simplifie à certains égards les choses.
Plusieurs processus exécutant chacun un seul thread d'exécution peuvent être bien meilleurs que plusieurs threads exécutés dans un seul processus. Et avec l'arrivée d'une grande partie du nouveau code peer-to-peer comme AMQP (RabbitMQ, Qpid, etc.) et ZeroMQ, il est beaucoup plus facile de diviser les threads entre différents espaces de processus et même des machines et des réseaux, ce qui simplifie considérablement les choses. Mais ce n'est pas une solution miracle. Il y a encore de la complexité à gérer. Vous venez de déplacer certaines de vos variables de l'espace de processus vers le réseau.
L'essentiel est que la décision d'entrer dans le domaine des threads n'est pas légère. Une fois que vous entrez dans ce territoire, presque instantanément tout devient plus complexe et de nouvelles races de problèmes entrent dans votre vie. Cela peut être amusant et cool, mais c'est comme l'énergie nucléaire - quand les choses tournent mal, elles peuvent aller mal et vite. Je me souviens d'avoir suivi un cours de formation à la criticité il y a de nombreuses années et ils ont montré des photos de certains scientifiques de Los Alamos qui jouaient avec du plutonium dans les laboratoires de la Seconde Guerre mondiale. Beaucoup ont pris peu ou pas de précautions contre une exposition, et en un clin d'œil - en un seul flash lumineux et indolore, tout serait fini pour eux. Quelques jours plus tard, ils étaient morts. Richard Feynman l'a appelé plus tard " chatouillant la queue du dragon ." C'est un peu ce à quoi peut ressembler jouer avec des threads (du moins pour moi de toute façon). Cela semble plutôt inoffensif au début, et au moment où vous êtes mordu, vous vous grattez la tête à la rapidité avec laquelle les choses ont mal tourné. Mais au moins, les fils ne vous tueront pas.
Tout d'abord, une application à thread unique ne tirera jamais parti d'un processeur multicœur ou d'un hyper-threading. Mais même sur un seul cœur, le processeur à un seul thread faisant du multi-thread présente des avantages.
Considérez l'alternative et si cela vous rend heureux. Supposons que vous ayez plusieurs tâches à exécuter simultanément. Par exemple, vous devez continuer à communiquer avec deux systèmes différents. Comment faire cela sans multi-threading? Vous devez probablement créer votre propre planificateur et le laisser appeler les différentes tâches qui doivent être effectuées. Cela signifie que vous devez diviser vos tâches en plusieurs parties. Vous devez probablement respecter certaines contraintes en temps réel, vous devez vous assurer que vos pièces ne prennent pas trop de temps. Sinon, le minuteur expirera dans d'autres tâches. Cela rend la division d'une tâche plus difficile. Plus vous devez gérer vous-même de tâches, plus vous devez vous séparer et plus votre ordonnanceur deviendra complexe pour répondre à toutes les contraintes.
Lorsque vous avez plusieurs threads, la vie peut devenir plus facile. Un planificateur préemptif peut arrêter un thread à tout moment, conserver son état et re (démarrer) un autre. Il redémarrera lorsque votre thread aura son tour. Avantages: la complexité de l'écriture d'un planificateur a déjà été faite pour vous et vous n'avez pas à répartir vos tâches. De plus, l'ordonnanceur est capable de gérer des processus/threads dont vous n'êtes même pas au courant. Et aussi, lorsqu'un thread n'a rien à faire (il attend un événement), il ne prendra aucun cycle CPU. Ce n'est pas si facile à réaliser lorsque vous créez votre ordonnanceur à un seul thread. (endormir n'est pas si difficile, mais comment se réveille-t-il?)
L'inconvénient du développement multithread est que vous devez comprendre les problèmes de concurrence, les stratégies de verrouillage, etc. Développer un code multi-thread sans erreur peut être assez difficile. Et le débogage peut être encore plus difficile.
existe-t-il quelque chose qui ne peut être accompli qu'en utilisant plusieurs threads?
Oui. Vous ne pouvez pas exécuter de code sur plusieurs processeurs ou cœurs de processeur avec un seul thread.
Sans plusieurs processeurs/cœurs, les threads peuvent toujours simplifier le code qui s'exécute conceptuellement en parallèle, comme la gestion des clients sur un serveur - mais vous pouvez faire la même chose sans threads.
Les discussions ne concernent pas seulement la vitesse, mais aussi la concurrence.
Si vous n'avez pas d'application batch comme @Peter l'a suggéré, mais plutôt une boîte à outils GUI comme WPF, comment interagir avec les utilisateurs et la logique métier avec un seul thread?
Supposons également que vous créez un serveur Web. Comment serviriez-vous plus d'un utilisateur simultanément avec un seul thread (en supposant qu'aucun autre processus)?
Il existe de nombreux scénarios où un seul thread simple ne suffit pas. C'est pourquoi des avancées récentes telles que le processeur Intel MIC avec plus de 50+ cœurs et des centaines de threads ont lieu.
Oui, la programmation parallèle et simultanée est difficile. Mais nécessaire.
Le multithreading peut permettre à l'interface graphique d'être toujours réactive pendant de longues opérations de traitement. Sans multithread, l'utilisateur serait bloqué en regardant un formulaire verrouillé pendant qu'un long processus est en cours d'exécution.
Le code multithread peut bloquer la logique du programme et accéder aux données périmées de manière impossible pour les threads uniques.
Les threads peuvent prendre un bug obscur de quelque chose qu'un programmeur moyen peut déboguer et le déplacer dans le domaine où les histoires sont racontées sur la chance nécessaire pour attraper le même bug avec son pantalon baissé lorsqu'un programmeur d'alerte regardait justement le bon moment.
les applications traitant du blocage IO qui doivent également rester réactives aux autres entrées (l'interface graphique ou d'autres connexions) ne peuvent pas être rendues simples
l'ajout de méthodes de vérification dans la bibliothèque IO pour voir combien peut être lu sans blocage peut aider cela, mais peu de bibliothèques ne font aucune garantie complète à ce sujet
Beaucoup de bonnes réponses, mais je ne suis pas sûr de le dire comme je le ferais - Peut-être que cela offre une façon différente de voir les choses:
Les threads sont juste une simplification de programmation comme des objets ou des acteurs ou pour des boucles (Oui, tout ce que vous implémentez avec des boucles vous pouvez l'implémenter avec if/goto).
Sans threads, vous implémentez simplement un moteur d'état. J'ai dû le faire plusieurs fois (la première fois que je l'ai fait, je n'en ai jamais entendu parler - je viens de faire une grosse déclaration de changement contrôlée par une variable "State"). Les machines à états sont encore assez courantes mais peuvent être gênantes. Avec des fils, un énorme morceau du passe-partout disparaît.
Il arrive également qu'il soit plus facile pour un langage de diviser son exécution en morceaux conviviaux multi-CPU (tout comme les acteurs, je crois).
Java fournit des threads "verts" sur les systèmes où le système d'exploitation ne fournit AUCUN support de thread. Dans ce cas, il est plus facile de voir qu'ils ne sont clairement rien de plus qu'une abstraction de programmation.
Premièrement, les threads peuvent faire deux choses ou plus en même temps (si vous avez plus d'un noyau). Bien que vous puissiez également le faire avec plusieurs processus, certaines tâches ne se répartissent pas très bien sur plusieurs processus.
De plus, certaines tâches contiennent des espaces que vous ne pouvez pas facilement éviter. Par exemple, il est difficile de lire les données d'un fichier sur le disque et de faire en sorte que votre processus fasse autre chose en même temps. Si votre tâche nécessite nécessairement beaucoup de données de lecture sur le disque, votre processus passera beaucoup de temps à attendre le disque, quoi que vous fassiez.
Deuxièmement, les threads peuvent vous éviter d'avoir à optimiser de grandes quantités de votre code qui ne sont pas critiques en termes de performances. Si vous n'avez qu'un seul thread, chaque morceau de code est critique en termes de performances. S'il bloque, vous êtes coulé - aucune tâche qui serait effectuée par ce processus ne peut avancer. Avec les threads, un bloc n'affectera que ce thread et d'autres threads peuvent venir et travailler sur les tâches qui doivent être effectuées par ce processus.
Un bon exemple est le code de gestion des erreurs rarement exécuté. Supposons qu'une tâche rencontre une erreur très rare et que le code pour gérer cette erreur doit être paginé en mémoire. Si le disque est occupé et que le processus n'a qu'un seul thread, aucune progression vers l'avant ne peut être effectuée tant que le code pour gérer cette erreur ne peut pas être chargé en mémoire. Cela peut provoquer une réponse éclatante.
Un autre exemple est si vous devez très rarement effectuer une recherche dans la base de données. Si vous attendez que la base de données réponde, votre code subira un énorme retard. Mais vous ne voulez pas vous donner la peine de rendre tout ce code asynchrone car il est si rare que vous devez effectuer ces recherches. Avec un fil pour faire ce travail, vous obtenez le meilleur des deux mondes. Un thread pour effectuer ce travail le rend non critique comme il se doit.
Les systèmes d'exploitation utilisent le concept de découpage temporel où chaque thread obtient son temps pour s'exécuter, puis est préempté. Une telle approche peut remplacer le filetage tel qu'il est actuellement, mais écrire vos propres ordonnanceurs dans chaque application serait exagéré. De plus, vous devez travailler avec des périphériques d'E/S et ainsi de suite. Et nécessiterait une certaine prise en charge du côté matériel, afin que vous puissiez déclencher des interruptions pour faire fonctionner votre planificateur. Fondamentalement, vous écririez un nouveau système d'exploitation à chaque fois.
En général, le thread peut améliorer les performances dans les cas où les threads attendent des E/S ou sont en veille. Il vous permet également de créer des interfaces réactives et de permettre l'arrêt des processus pendant que vous effectuez de longues tâches. De plus, le threading améliore les choses sur les vrais processeurs multicœurs.