Multithreading: Quel est le point de plus de fils que de noyaux?
j'ai pensé que le but d'un ordinateur multi-core est qu'il puisse exécuter plusieurs threads simultanément. Dans ce cas, si vous avez une machine quad-core, à quoi sert d'avoir plus de 4 threads à la fois? Ne sont-ils pas en train de voler du temps l'un à l'autre?
17 réponses
ce n'est pas parce qu'un thread existe qu'il est actif. De nombreuses applications de threads impliquent que certains threads vont dormir jusqu'à ce qu'il soit temps pour eux de faire quelque chose - par exemple, l'utilisateur input déclenchant les threads pour se réveiller, faire un peu de traitement, et retourner au Sommeil.
essentiellement, les threads sont des tâches individuelles qui peuvent fonctionner indépendamment les unes des autres, sans qu'il soit nécessaire d'être conscient de l'avancement d'une autre tâche. Il est tout à fait possible de avoir plus de ceux-ci que vous avez la capacité de courir simultanément; ils sont toujours utiles pour la commodité, même s'ils ont parfois à faire la queue derrière l'autre.
la réponse tourne autour du but des fils, qui est le parallélisme: exécuter plusieurs lignes d'exécution à la fois. Dans un système "idéal", vous auriez un thread d'exécution par cœur: pas d'interruption. En réalité, ce n'est pas le cas. Même si vous avez quatre noyaux et quatre threads de travail, votre processus et les threads it seront constamment commutés pour d'autres processus et threads. Si vous utilisez un OS moderne, chaque processus a au moins un fil, et de nombreux en avoir plus. Tous ces processus fonctionnent en même temps. Vous avez probablement plusieurs centaines de fils qui tournent tous sur votre machine en ce moment. Vous n'obtiendrez jamais une situation où un fil fonctionne sans avoir le temps "volé" de lui. (Eh bien , vous pourriez si c'est tournant en temps réel , si vous utilisez un OS en temps réel ou, même sur Windows, utilisez une priorité de thread en temps réel. Mais il est rare.)
avec cela comme arrière-plan, la réponse: oui, plus de quatre fils sur un vrai la machine à quatre cœurs peut vous donner une situation où ils 'volent le temps l'un de l'autre', mais seulement si chaque fil a besoin de 100% CPU . Si un thread ne fonctionne pas à 100% (comme un thread UI pourrait ne pas fonctionner, ou un thread faisant une petite quantité de travail ou attendant quelque chose d'autre) alors un autre thread étant prévu est en fait une bonne situation.
c'est en fait plus compliqué que ça:
-
Que faire si vous avez-vous cinq pièces de travail à faire en même temps? Il est plus logique de les exécuter tous en même temps que d'en Exécuter quatre et d'en exécuter le cinquième plus tard.
-
il est rare qu'un fil ait vraiment besoin de 100% CPU. Au moment où il utilise le disque ou l'E/S réseau, par exemple, il peut être potentiellement passer le temps à attendre de ne rien faire d'utile. C'est une situation très courante.
-
si vous avez un travail qui nécessite être exécuté, un mécanisme commun est d'utiliser un pool de threads. Il semble logique d'avoir le même nombre de threads que de cœurs, encore .Net threadpool a jusqu'à 250 threads par processeur . Je ne suis pas certain de la raison pour laquelle ils font cela, mais ma supposition est de faire avec la taille des tâches qui sont donnés pour exécuter sur les fils.
ainsi: voler le temps n'est pas une mauvaise chose (et n'est pas vraiment le vol, non plus: c'est comment le système est censé fonctionner.) Écrivez vos programmes multithreaded en fonction du type de travail que les threads vont faire, qui peut ne pas être lié au CPU. Déterminez le nombre de fils dont vous avez besoin en fonction du profilage et de la mesure. Vous pouvez trouver plus utile de penser en termes de tâches ou de travaux, plutôt que de threads: écrivez des objets de travail et donnez-les à un pool à exécuter. Enfin, à moins que votre programme ne soit vraiment critique pour la performance, ne vous inquiétez pas trop:)
le point est que, bien que n'obtenant pas de vitesse réelle lorsque le nombre de thread dépasse le nombre de noyau, vous pouvez utiliser des threads pour démêler des morceaux de logique qui ne devraient pas avoir à être interdépendants.
même dans une application modérément complexe, en utilisant un seul thread essayer de tout faire rapidement fait hachage du "flux" de votre code. Le fil simple passe la plupart de son temps à sonder ceci, à vérifier cela, à appeler des routines conditionnellement si nécessaire, et il devient difficile rien voir mais un marécage de minuties.
comparez ceci avec le cas où vous pouvez dédier des threads à des tâches de sorte que, en regardant n'importe quel thread individuel, vous pouvez voir ce que ce thread est en train de faire. Par exemple, un thread peut bloquer l'attente d'une entrée à partir d'une socket, analyser le flux dans les messages, filtrer les messages, et lorsqu'un message valide arrive, le transmettre à un autre worker thread. Le thread worker peut travailler sur des entrées provenant d'un certain nombre d'autres sources. Le code de chacun d'entre eux présentera un flux propre et déterminé, sans avoir à faire des vérifications explicites qu'il n'y a rien d'autre à faire.
partitionner le travail de cette façon permet à votre application de s'appuyer sur le système d'exploitation pour programmer ce qu'il faut faire ensuite avec le cpu, de sorte que vous n'avez pas à faire des contrôles conditionnels explicites partout dans votre application sur ce qui pourrait bloquer et ce qui est prêt à être traité.
si un thread attend une ressource (telle que le chargement D'une valeur de RAM dans un registre, l'entrée/sortie du disque, l'accès au réseau, le lancement d'un nouveau processus, la requête d'une base de données, ou l'attente de l'entrée de l'utilisateur), le processeur peut travailler sur un thread différent, et revenir au premier thread une fois que la ressource est disponible. Cela réduit le temps que le CPU passe au ralenti, car le CPU peut effectuer des millions d'opérations au lieu de rester au ralenti.
envisager un thread qui a besoin de lire les données sur un dur lecteur. En 2014, un cœur de processeur typique fonctionne à 2,5 GHz et peut être en mesure d'exécuter 4 instructions par cycle. Avec un temps de cycle de 0,4 ns, le processeur peut exécuter 10 instructions par nanoseconde. Avec un disque dur mécanique typique les temps de recherche sont d'environ 10 millisecondes, le processeur est capable d'exécuter 100 millions d'instructions dans le temps qu'il faut pour lire une valeur à partir du disque dur. Il peut y avoir des améliorations significatives de la performance avec des disques durs avec un petit cache (tampon de 4 Mo) et les lecteurs hybrides avec quelques Go de stockage, comme la latence des données pour les lectures séquentielles ou lit de la section hybride peut être plusieurs ordres de grandeur plus rapide.
un noyau de processeur peut basculer entre les threads (Le coût pour l'arrêt et la reprise d'un thread est d'environ 100 cycles d'horloge) tandis que le premier thread attend une entrée de haute latence (rien de plus cher que les registres (1 Horloge) et la mémoire vive (5 nanosecondes)) ceux-ci comprennent l'E/S du disque, l'accès au réseau( latence de 250ms), la lecture des données sur un CD ou un bus lent, ou un appel de la base de données. Avoir plus de threads que de noyaux signifie que le travail utile peut être fait tandis que les tâches à haute latence sont résolues.
le CPU a un programmeur de thread qui attribue la priorité à chaque thread, et permet à un thread de dormir, puis de reprendre après un temps prédéterminé. C'est le travail de l'ordonnanceur de thread de réduire le grincement, qui se produirait si chaque thread exécuté seulement 100 instructions avant d'être mis au lit à nouveau. Le plafond des fils de commutation réduirait le débit utile total du cœur du processeur.
Pour cette raison, vous pouvez casser votre problème à un nombre raisonnable de threads. Si vous écriviez du code pour effectuer la multiplication matricielle, la création d'un thread par cellule dans la matrice de sortie pourrait être excessive, alors qu'un thread par ligne ou par lignes n dans la matrice de sortie pourrait réduire le coût de création, d'arrêt et de reprise des threads.
C'est aussi pourquoi la prédiction par branche est importante. Si vous avez une instruction if qui nécessite de charger une valeur à partir de la RAM mais que le corps des instructions if et else utilise des valeurs déjà chargées dans les registres, le processeur peut exécuter une ou deux branches avant que la condition ait été évaluée. Une fois la condition retournée, le processeur appliquera le résultat de la branche correspondante et rejettera l'autre. Effectuer éventuellement un travail inutile ici est probablement mieux que de passer à un un fil différent, qui pourrait conduire à des coups.
comme nous nous sommes éloignés des processeurs monochromes à haute vitesse d'horloge aux processeurs multi-noyaux, la conception de puce a mis l'accent sur le criblage plus de noyaux par matrice, l'amélioration du partage des ressources sur puce entre les noyaux, de meilleurs algorithmes de prédiction de branche, une meilleure commutation de fil au-dessus de la tête, et une meilleure planification de fil.
je suis tout à fait en désaccord avec l'affirmation de @kyoryu que le nombre idéal est un fil par CPU.
pensez-y de cette façon: Pourquoi avons-nous des systèmes d'exploitation multi-processus? Pour la plupart de l'histoire de l'ordinateur, presque tous les ordinateurs avaient un CPU. Pourtant, à partir des années 1960, tous les ordinateurs "réels" possédaient des systèmes d'exploitation multi-processing (aka multi-tasking).
vous exécutez plusieurs programmes de sorte que l'un peut exécuter tandis que d'autres sont bloqués pour des choses comme IO.
laisse de côté les arguments à savoir si les versions Windows avant NT étaient multi-tâches. Depuis lors, chaque vrai OS a eu multi-tâches. Certains ne l'exposent pas aux utilisateurs, mais il est là quand même, en faisant des choses comme écouter la radio du téléphone cellulaire, parler à la puce GPS, accepter l'entrée de souris, etc.
filetage sont les tâches qui sont un peu plus efficaces. Il n'y a pas de différence fondamentale entre une tâche, un processus et un fil.
A L'UC est une chose terrible à gaspiller, donc beaucoup de choses prêt à l'utiliser quand vous le pouvez.
je conviens qu'avec la plupart des langages de procédure, C, C++, Java, etc, écrire un code de thread sûr est beaucoup de travail. Avec 6 CPU de base sur le marché aujourd'hui, et 16 CPU de base pas loin, je m'attends à ce que les gens vont s'éloigner de ces vieilles langues, comme multi-threading est de plus en plus d'une exigence critique.
désaccord avec @kyoryu est juste IMHO, le reste est fait.
Bien que vous pouvez certainement utiliser des threads pour accélérer les calculs en fonction de votre matériel, l'une de leurs principales utilisations est de faire plus d'une chose à un moment de convivialité raisons.
par exemple, si vous devez effectuer un traitement en arrière-plan et que vous restez sensible à L'entrée de L'UI, vous pouvez utiliser des threads. Sans threads, l'interface utilisateur serait suspendu à chaque fois que vous avez essayé de faire un traitement lourd.
Voir aussi question connexe: utilisations pratiques pour les fils
Imaginez un serveur Web qui doit servir un nombre arbitraire de requêtes. Vous devez servir les demandes en parallèle parce que sinon chaque nouvelle demande doit attendre jusqu'à ce que toutes les autres demandes ont été complétées (y compris l'envoi de la réponse sur Internet). Dans ce cas, la plupart des serveurs web ont beaucoup moins de noyaux que le nombre de requêtes qu'ils servent habituellement.
Il rend également plus facile pour le développeur du serveur: Vous n'avez qu'à écrire un fil programme qui sert une requête, vous n'avez pas à penser à stocker plusieurs requêtes, l'ordre dans lequel vous les servez, et ainsi de suite.
la plupart des réponses ci-dessus parlent de performance et de fonctionnement simultané. Je vais aborder ça sous un autre angle.
prenons le cas, disons, d'un programme d'émulation de terminal simpliste. Vous devez faire les choses suivantes:
- guetter les caractères entrants du système distant et les afficher
- surveillez les éléments provenant du clavier et envoyez-les au système à distance
(de vrais émulateurs de terminaux font plus, y compris potentiellement l'écho de la substance que vous tapez sur l'écran ainsi, mais nous allons passer au-dessus de cela pour le moment.)
maintenant la boucle pour la lecture à partir de la télécommande est simple, comme par le pseudo suivant:
while get-character-from-remote:
print-to-screen character
la boucle pour surveiller le clavier et l'envoi est aussi simple:
while get-character-from-keyboard:
send-to-remote character
le problème, cependant, est que vous devez faire ceci simultanément. Le code doit maintenant ressembler à ceci si vous n'avez pas de filetage:
loop:
check-for-remote-character
if remote-character-is-ready:
print-to-screen character
check-for-keyboard-entry
if keyboard-is-ready:
send-to-remote character
la logique, même dans cet exemple délibérément simplifié qui ne tient pas compte de la complexité des communications dans le monde réel, est assez confuse. Avec le filetage, cependant, même sur un seul noyau, les deux boucles de pseudocode peuvent exister indépendamment sans entrelacer leur logique. Puisque les deux threads seront la plupart du temps I / O-lié, ils ne mettent pas une lourde charge sur le CPU, même bien qu'ils soient, à proprement parler, plus gaspilleurs de ressources CPU que ne le serait la boucle intégrée.
maintenant, bien sûr, l'usage dans le monde réel est plus compliqué que ce qui précède. Mais la complexité de la boucle intégrée augmente de façon exponentielle à mesure que vous ajoutez des préoccupations à l'application. La logique devient de plus en plus fragmentée et vous devez commencer à utiliser des techniques comme les machines d'état, coroutines, et autres pour obtenir des choses gérables. Gérable, mais pas lisible. Filetage garde la code plus lisible.
alors pourquoi ne pas utiliser de filetage?
Eh bien, si vos tâches sont liées par CPU au lieu de I/O, le threading ralentit en fait votre système. Performances s'en ressentiront. Beaucoup, dans de nombreux cas. ("Thrashing" est un problème courant si vous laissez tomber trop de threads liés au CPU. Vous passez plus de temps à changer les threads actifs qu'à exécuter le contenu des threads eux-mêmes.) En outre, l'une des raisons de la logique ci-dessus est si simple est que j'ai délibérément choisi un exemple simpliste (et irréaliste). Si vous voulez faire écho à ce qui a été tapé à l'écran, alors vous avez un nouveau monde de souffrance que vous introduisez verrouillage des ressources partagées. Avec seulement une ressource partagée, ce n'est pas tellement un problème, mais il commence à devenir de plus en plus gros problème que vous avez plus de ressources à partager.
ainsi, à la fin, filetage est sur beaucoup de choses. Par exemple, il s'agit de faire des I/O-bound des processus plus réactif (même si moins efficace dans son ensemble) comme certains l'ont déjà dit. Il s'agit aussi de rendre la logique plus facile à suivre (mais seulement si vous minimisez l'état partagé). Il s'agit de beaucoup de choses, et vous devez décider si ses avantages l'emportent sur ses inconvénients au cas par cas.
beaucoup de threads seront endormis, attendant les entrées utilisateur, les entrées/sorties, et d'autres événements.
Threads peut aider à la réactivité dans les applications D'UI. En outre, vous pouvez utiliser des fils pour obtenir plus de travail de vos noyaux. Par exemple, sur un seul noyau, vous pouvez avoir un thread faisant IO et un autre faisant du calcul. S'il s'agissait d'un filetage simple, le noyau pourrait être essentiellement inactif en attendant que l'IO soit terminé. C'est un exemple assez haut de gamme, mais les threads peuvent certainement être utilisés pour taper votre cpu un peu plus fort.
un processeur, ou CPU, est la puce physique qui est branchée sur le système. Un processeur peut avoir plusieurs cœurs (un noyau est la partie de la puce qui est capable d'exécuter des instructions). Un noyau peut apparaître au système d'exploitation comme plusieurs processeurs virtuels s'il est capable d'exécuter simultanément plusieurs threads (un thread est une séquence unique d'instructions).
Un processus est un autre nom pour une application. Généralement, les processus sont indépendants de mutuellement. Si un processus meurt, il n'entraîne pas la mort d'un autre processus. Il est possible pour les processus de communiquer, ou de partager des ressources telles que la mémoire ou I/O.
chaque procédé comporte un espace d'adresse et une pile distincts. Un processus peut contenir plusieurs threads, chacun pouvant exécuter des instructions simultanément. Tous les threads d'un processus partagent le même espace d'adressage, mais chaque thread a son propre pile.
avec ces définitions et d'autres recherches utilisant ces principes de base vous aideront à comprendre.
l'usage idéal des threads est, en effet, un par noyau.
cependant, à moins que vous utilisiez exclusivement IO asynchrone/non-bloquant, il y a de bonnes chances que vous ayez des threads bloqués sur IO à un moment donné, qui n'utiliseront pas votre CPU.
de plus, les langages de programmation typiques rendent difficile l'utilisation d'un thread par CPU. Les langages conçus autour de la simultanéité (comme L'Erlang) peuvent rendre plus facile de ne pas utiliser de threads supplémentaires.
de la façon dont certaines API sont conçues, vous avez pas de choix mais pour les exécuter dans un thread séparé (n'importe quoi avec des opérations de blocage). Un exemple serait les bibliothèques HTTP de Python (AFAIK).
en général, ce n'est pas vraiment un problème (si c'est un problème, L'OS ou L'API devrait envoyer avec un mode de fonctionnement asynchrone alternatif, i.e.: select(2)
), parce que cela signifie probablement que le fil va être endormi pendant l'attente d'E/S achèvement. D'un autre côté, si quelque chose fait un calcul lourd, vous avez pour le mettre dans un fil séparé que dire, le fil GUI (à moins que vous appréciez le multiplexage manuel).
en réponse à votre première conjecture: les machines multi-core peuvent exécuter simultanément plusieurs processus, pas seulement les fils multiples d'un seul processus.
en réponse à votre première question: le point de fils multiples est généralement d'effectuer simultanément plusieurs tâches au sein d'une même application. Les exemples classiques sur le net sont un programme de courrier électronique d'envoi et de réception de courrier, et un serveur web de réception et d'envoi de demandes de page. (Notez qu'il est essentiellement impossible de réduire un système comme Windows à un seul thread ou même un seul processus. Exécutez le Gestionnaire de tâches Windows et vous verrez typiquement une longue liste de processus actifs, dont beaucoup tourneront plusieurs threads.)
en réponse à votre deuxième question: la plupart des processus/threads ne sont pas liés au CPU (c'est-à-dire qu'ils ne fonctionnent pas en continu et sans interruption), mais s'arrêtent et attendent fréquemment que les e/s se terminent. Pendant cette attente, d'autres les processus / threads peuvent s'exécuter sans "voler" le code d'attente (même sur une machine à noyau unique).
je sais que c'est une super vieille question avec beaucoup de bonnes réponses, mais je suis ici pour souligner quelque chose qui est important dans l'environnement actuel:
si vous voulez concevoir une application pour multi-threading, vous ne devriez pas concevoir pour un réglage matériel spécifique. La technologie CPU progresse assez rapidement depuis des années, et le nombre de noyaux augmente régulièrement. Si vous concevez délibérément votre application de sorte qu'il utilise seulement 4 threads, alors vous êtes possibilité de vous restreindre dans un système octa-core (par exemple). Aujourd'hui, même les systèmes à 20 cœurs sont disponibles sur le marché, donc un tel design fait certainement plus de mal que de bien.
un thread est une abstraction qui vous permet d'écrire du code aussi simple qu'une séquence d'opération, ignorant totalement que le code est exécuté entrelacé avec d'autres codes.
le fait est que la grande majorité des programmeurs ne comprennent pas comment concevoir une machine d'état. Être capable de tout mettre dans son propre thread libère le programmeur d'avoir à réfléchir sur la façon de représenter efficacement l'état des différents calculs en cours afin qu'ils puissent être interrompus et repris plus tard.
par exemple, considérez la compression vidéo, une tâche très intensive en cpu. Si vous utilisez un outil gui, vous voulez probablement l'interface pour demeurer réceptif (montrer les progrès, répondre aux demandes d'annulation, redimensionner les fenêtres, etc.).). Ainsi, vous concevez votre logiciel d'encodeur pour traiter une grande unité (une ou plusieurs images) à la fois et l'exécuter dans son propre thread, séparé de L'interface utilisateur.
bien sûr, une fois que vous réalisez qu'il aurait été agréable de pouvoir enregistrer l'état d'encodage en cours de sorte que vous pouvez fermer le programme pour redémarrer ou jouer à un jeu avide de ressources, vous réalisez que vous auriez dû apprendre à concevoir des machines d'état de début. Soit cela, soit vous décidez de concevoir un tout nouveau problème de processus-hibernation votre OS afin que vous puissiez suspendre et reprendre des applications individuelles sur le disque...