Multithreading: à quoi servent plus de threads que de cœurs?

Je pensais que le point d’un ordinateur multi-core était qu’il pouvait exécuter plusieurs threads simultanément. Dans ce cas, si vous avez une machine quad-core, à quoi bon avoir plus de 4 threads à la fois? Ne seraient-ils pas simplement en train de voler le temps l’un de l’autre?

Le simple fait qu’un thread existe ne signifie pas toujours qu’il est actif. De nombreuses applications de threads impliquent la mise en veille de certains threads, par exemple, le déclenchement de threads par les utilisateurs pour se réveiller, le traitement des données et le retour au sumil.

Essentiellement, les threads sont des tâches individuelles pouvant fonctionner indépendamment les unes des autres, sans qu’il soit nécessaire de connaître les progrès d’une autre tâche. Il est tout à fait possible d’en avoir plus que vous ne pouvez en faire simultanément; Ils sont toujours utiles pour des raisons pratiques, même s’ils doivent parfois faire la queue.

La réponse tourne autour du but des threads, qui est le parallélisme: exécuter plusieurs lignes d’exécution à la fois. Dans un système «idéal», vous auriez un thread en cours d’exécution par cœur: pas d’interruption. En réalité, ce n’est pas le cas. Même si vous avez quatre cœurs et quatre threads de travail, votre processus et ses threads seront constamment désactivés pour d’autres processus et threads. Si vous utilisez un système d’exploitation moderne, chaque processus a au moins un thread et beaucoup en ont plus. Tous ces processus fonctionnent en même temps. Vous avez probablement plusieurs centaines de threads sur votre machine en ce moment. Vous ne serez jamais confronté à une situation où un thread s’exécute sans que le temps en soit «volé». (Eh bien, vous pouvez le faire en temps réel , si vous utilisez un système d’exploitation temps réel ou, même sous Windows, utilisez une priorité de thread en temps réel. Mais c’est rare.)

Avec cela comme arrière-plan, la réponse: Oui, plus de quatre threads sur un véritable ordinateur à quatre cœurs peuvent vous donner une situation où ils «volent du temps l’un de l’autre», mais seulement si chaque thread a besoin d’un processeur à 100% . Si un thread ne fonctionne pas à 100% (comme un thread d’interface utilisateur peut ne pas l’être, ou qu’un thread effectue une petite quantité de travail ou attend quelque chose d’autre), un autre thread en cours de planification est en fait une bonne situation.

C’est en fait plus compliqué que ça:

  • Que se passe-t-il si vous avez cinq tâches à accomplir en même temps? Il est plus judicieux de les exécuter tous en même temps, que d’en exécuter quatre et de lancer le cinquième plus tard.

  • Il est rare qu’un thread ait réellement besoin d’un processeur 100%. Au moment où il utilise des E / S disque ou réseau, par exemple, il peut passer du temps à attendre sans rien faire d’utile. C’est une situation très courante.

  • Si vous avez du travail à exécuter, un mécanisme commun consiste à utiliser un pool de threads. Il peut sembler logique d’avoir le même nombre de threads que les cœurs, mais le pool de threads .Net a jusqu’à 250 threads disponibles par processeur . Je ne suis pas sûr de savoir pourquoi ils le font, mais je suppose que cela concerne la taille des tâches à exécuter sur les threads.

Donc, le temps de vol n’est pas une mauvaise chose (et ce n’est pas vraiment du vol non plus: c’est la façon dont le système est censé fonctionner.) Ecrivez vos programmes multithread en fonction du type de travail que les threads vont effectuer. -lié. Déterminez le nombre de threads 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 tâches, plutôt que de tâches: écrivez des objects de travail et donnez-les à un pool à exécuter. Enfin, à moins que votre programme ne soit vraiment critique en termes de performances, ne vous inquiétez pas trop 🙂

Le fait est que, bien qu’il n’y ait pas vraiment d’accélération lorsque le nombre de threads dépasse le nombre de core, vous pouvez utiliser des threads pour démêler des éléments de logique qui ne devraient pas être interdépendants.

Même dans une application moyennement complexe, l’utilisation d’un seul thread essaie de tout faire rapidement, rendant le “stream” de votre code difficile. Le thread unique passe le plus clair de son temps à interroger cela, à vérifier cela, à appeler les routines de manière conditionnelle si nécessaire, et il devient difficile de voir autre chose qu’un brouhaha de minuties.

Comparez cela avec le cas où vous pouvez dédier des threads à des tâches afin que, en regardant un thread individuel, vous puissiez voir ce que fait ce thread. Par exemple, un thread peut bloquer l’attente d’une entrée d’un socket, parsingr le stream en messages, filtrer les messages et lorsqu’un message valide est transmis, le transmettre à un autre thread de travail. Le thread de travail peut travailler sur des entrées provenant d’autres sources. Le code de chacun d’eux affichera un stream propre et ciblé, sans qu’il soit nécessaire de vérifier explicitement qu’il n’y a rien d’autre à faire.

Partitionner le travail de cette manière permet à votre application de compter sur le système d’exploitation pour planifier ce qu’il convient de faire ensuite avec le processeur. Vous n’avez donc pas à effectuer de vérifications conditionnelles explicites dans votre application.

Si un thread attend une ressource (par exemple, charger une valeur de la RAM dans un registre, entrée / sortie disque, access réseau, lancer un nouveau processus, interroger une firebase database ou attendre une entrée 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 pendant lequel le processeur passe en veille, car le processeur peut effectuer des millions d’opérations au lieu de restr inactif.

Considérons un thread qui doit lire des données sur un disque dur. En 2014, un cœur de processeur typique fonctionne à 2,5 GHz et peut 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 des temps de recherche de disque dur mécaniques typiques d’environ 10 millisecondes, le processeur est capable d’exécuter 100 millions d’instructions pendant le temps qu’il faut pour lire une valeur à partir du disque dur. Il peut y avoir des améliorations de performances significatives avec des disques durs avec un petit cache (4 Mo de mémoire tampon) et des disques hybrides avec quelques Go de stockage, car la latence des données pour les lectures séquentielles peut être plusieurs fois plus rapide.

Un cœur de processeur peut basculer entre les threads (le coût de la pause et de la reprise d’un thread est d’environ 100 cycles) tandis que le premier attend une entrée à haute latence (plus chère que les registres (1 horloge) et la RAM (5 nanosecondes)) E / S disque, access réseau (latence de 250 ms), lecture de données sur un CD ou un bus lent, ou appel de firebase database. Avoir plus de threads que de cœurs signifie qu’un travail utile peut être effectué pendant que les tâches à latence élevée sont résolues.

Le CPU dispose d’un programmateur de thread qui atsortingbue une priorité à chaque thread et permet à un thread de s’endormir, puis de reprendre après un temps prédéterminé. C’est le travail du planificateur de threads de réduire les attaques, ce qui se produirait si chaque thread n’exécutait que 100 instructions avant de se remettre en sumil. La surcharge du changement de thread réduirait le débit utile total du cœur du processeur.

Pour cette raison, vous pouvez diviser votre problème en un nombre raisonnable de threads. Si vous écriviez du code pour effectuer une multiplication masortingcielle, la création d’un thread par cellule dans la masortingce de sortie pourrait être excessive, tandis qu’un thread par ligne ou par n lignes dans la masortingce de sortie réduirait les coûts de création, de pause et de reprise des threads.

C’est aussi pourquoi la prédiction de la twig est importante. Si vous avez une instruction if qui nécessite de charger une valeur depuis la RAM mais que le corps des instructions if et else utilise des valeurs déjà chargées dans des registres, le processeur peut exécuter une ou les deux twigs avant que la condition ait été évaluée. Une fois la condition rétablie, le processeur appliquera le résultat de la twig correspondante et éliminera l’autre. Effectuer un travail potentiellement inutile ici est probablement mieux que de passer à un autre thread, ce qui pourrait conduire à des remous.

Comme nous sums passés des processeurs monocœurs à haute vitesse à des processeurs multi-cœurs, la conception des puces a consisté à regrouper plus de cœurs par masortingce, à améliorer le partage des ressources entre cœurs, à améliorer les algorithmes de prédiction des twigs, et une meilleure planification des threads.

Je ne suis pas du tout d’accord avec l’affirmation de @kyoryu selon laquelle le nombre idéal est un thread par CPU.

Pensez-y de cette façon: pourquoi avons-nous des systèmes d’exploitation multi-traitement? Pour la plupart de l’historique de l’ordinateur, presque tous les ordinateurs avaient un seul processeur. Pourtant, à partir des années 1960, tous les ordinateurs “réels” possédaient des systèmes d’exploitation multi-traitement (aka multi-tâches).

Vous exécutez plusieurs programmes pour que l’un puisse s’exécuter alors que d’autres sont bloqués pour des choses comme IO.

Laissons de côté les arguments selon lesquels les versions de Windows antérieures à NT étaient multitâches. Depuis lors, chaque véritable OS avait plusieurs tâches. Certains ne l’exposent pas aux utilisateurs, mais ils y parviennent quand même, en écoutant la radio du téléphone portable, en discutant avec la puce GPS, en acceptant l’entrée de la souris, etc.

Les threads ne sont que des tâches un peu plus efficaces. Il n’y a pas de différence fondamentale entre une tâche, un processus et un thread.

Un processeur est une chose terrible à gaspiller, donc beaucoup de choses sont prêtes à l’utiliser quand vous le pouvez.

Je suis d’accord avec le fait qu’avec la plupart des langages procéduraux, C, C ++, Java, etc. Avec 6 processeurs de base sur le marché actuel et 16 processeurs de base non loin de là, je pense que les utilisateurs s’éloigneront de ces anciens langages, car le multi-threading est de plus en plus indispensable.

Désaccord avec @kyoryu est juste IMHO, le rest est fait.

Bien que vous puissiez certainement utiliser des threads pour accélérer les calculs en fonction de votre matériel, l’une de leurs principales utilisations consiste à faire plus d’une chose à la fois pour des raisons de convivialité.

Par exemple, si vous devez effectuer un traitement en arrière-plan et restr sensible à l’entrée de l’interface utilisateur, vous pouvez utiliser des threads. Sans threads, l’interface utilisateur se bloquerait à chaque fois que vous tenteriez de faire un traitement intensif.

Voir aussi cette question connexe: Utilisations pratiques des threads

Imaginez un serveur Web devant desservir un nombre arbitraire de requêtes. Vous devez servir les demandes en parallèle car sinon, chaque nouvelle demande doit attendre que toutes les autres requêtes soient terminées (y compris l’envoi de la réponse via Internet). Dans ce cas, la plupart des serveurs Web ont beaucoup moins de cœurs que le nombre de requêtes qu’ils servent habituellement.

Cela facilite également la tâche du développeur du serveur: vous n’avez qu’à écrire un programme de discussion qui répond à une requête, vous n’avez pas à penser à stocker plusieurs requêtes, l’ordre dans lequel vous les proposez, etc.

La plupart des réponses ci-dessus parlent de performance et de fonctionnement simultané. Je vais aborder cela sous un angle différent.

Prenons le cas, par exemple, d’un programme d’émulation de terminal simpliste. Vous devez faire les choses suivantes:

  • surveiller les caractères entrants du système distant et les afficher
  • Surveillez les éléments provenant du clavier et envoyez-les au système distant

(Les émulateurs de terminaux réels en font plus, y compris les échos que vous tapez sur l’affichage, mais nous en reparlerons pour le moment.)

Maintenant, la boucle pour la lecture de la télécommande est simple, selon le pseudocode suivant:

while get-character-from-remote: print-to-screen character 

La boucle de surveillance du clavier et de l’envoi est également simple:

 while get-character-from-keyboard: send-to-remote character 

Le problème, cependant, est que vous devez le faire simultanément. Le code doit maintenant ressembler plus à ceci si vous n’avez pas de thread:

 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 prend pas en compte la complexité réelle des communications, est assez obscure. Avec le threading, même sur un seul cœur, les deux boucles de pseudocode peuvent exister indépendamment sans entrelacer leur logique. Comme les deux threads seront principalement liés aux E / S, ils ne surchargent pas le processeur, même s’ils sont, à proprement parler, plus gourmands en ressources de processeur que la boucle intégrée.

Maintenant, bien sûr, l’utilisation du monde réel est plus compliquée que celle ci-dessus. Mais la complexité de la boucle intégrée augmente de façon exponentielle à mesure que vous ajoutez de nouvelles préoccupations à l’application. La logique devient de plus en plus fragmentée et vous devez commencer à utiliser des techniques telles que les machines à états, les coroutines et autres pour gérer les choses. Gérable, mais pas lisible. Threading garde le code plus lisible.

Alors pourquoi ne pas utiliser le threading?

Eh bien, si vos tâches sont liées au processeur plutôt que liées aux E / S, les threads ralentissent réellement votre système. La performance en souffrira. Beaucoup, dans de nombreux cas. (“Thrashing” est un problème courant si vous déposez trop de threads liés au CPU. Vous finissez par passer plus de temps à changer les threads actifs qu’en exécutant eux-mêmes le contenu des threads.) C’est si simple que j’ai très 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 en proie au blocage des ressources partagées. Avec une seule ressource partagée, ce n’est pas vraiment un problème, mais cela commence à devenir un problème de plus en plus important car vous avez plus de ressources à partager.

Donc, au final, le threading concerne beaucoup de choses. Par exemple, il s’agit de rendre les processus liés aux E / S plus réactifs (même s’ils sont globalement moins efficaces), comme certains l’ont déjà dit. Il s’agit également 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.

Les threads peuvent aider à la réactivité dans les applications d’interface utilisateur. De plus, vous pouvez utiliser des threads pour optimiser le travail de vos cœurs. Par exemple, sur un seul cœur, vous pouvez avoir un thread effectuant des E / S et un autre effectuant des calculs. S’il s’agissait d’un seul thread, le kernel pourrait essentiellement être inactif en attendant que l’IO se termine. C’est un exemple de très haut niveau, mais les threads peuvent certainement être utilisés pour battre un peu plus votre processeur.

Un processeur ou processeur est la puce physique connectée au système. Un processeur peut avoir plusieurs cœurs (un cœur est la partie de la puce capable d’exécuter des instructions). Un kernel peut apparaître au système d’exploitation sous la forme de plusieurs processeurs virtuels s’il est capable d’exécuter simultanément plusieurs threads (un thread est une séquence d’instructions unique).

Un processus est un autre nom pour une application. Généralement, les processus sont indépendants les uns des autres. Si un processus meurt, cela ne provoque pas la mort d’un autre processus. Les processus peuvent communiquer ou partager des ressources telles que la mémoire ou les E / S.

Chaque processus dispose d’un espace d’adressage et d’une stack séparés. 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 aura sa propre stack.

Nous espérons que ces définitions et d’autres recherches utilisant ces principes aideront votre compréhension.

De nombreux threads seront endormis, attendant une entrée utilisateur, des E / S et d’autres événements.

L’utilisation idéale des threads est en effet un par cœur.

Cependant, à moins que vous n’utilisiez exclusivement des IO asynchrones / non bloquantes, il y a de fortes chances que des threads soient bloqués sur IO à un moment donné, ce qui n’utilisera pas votre CPU.

En outre, les langages de programmation classiques rendent difficile l’utilisation d’un seul thread par CPU. Les langues conçues autour de la concurrence (comme Erlang) peuvent faciliter l’utilisation de threads supplémentaires.

La façon dont certaines API sont conçues, vous n’avez pas d’autre choix que de 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).

Habituellement, cela ne pose pas vraiment de problème (si c’est un problème, le système d’exploitation ou l’API doit être livré avec un autre mode de fonctionnement asynchrone, à savoir: select(2) ), en attente de l’achèvement des E / S. D’un autre côté, si quelque chose fait un calcul lourd, vous devez le mettre dans un thread séparé, par exemple, le thread de l’interface graphique (sauf si 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 multiples threads d’un même processus.

En réponse à votre première question: le but de plusieurs threads est généralement d’exécuter simultanément plusieurs tâches au sein d’une même application. Les exemples classiques sur le net sont un programme de messagerie envoyant et recevant du courrier, et un serveur Web recevant et envoyant des demandes de page. (Notez qu’il est essentiellement impossible de réduire un système tel que Windows à un seul thread ou à un seul processus. Exécutez le Gestionnaire de tâches Windows et vous verrez généralement une longue liste de processus actifs, dont plusieurs exécuteront plusieurs threads. )

En réponse à votre deuxième question: la plupart des processus / threads ne sont pas liés au processeur (c’est-à-dire, ils ne s’exécutent pas de manière continue et ininterrompue). Pendant cette attente, d’autres processus / threads peuvent s’exécuter sans “voler” le code en attente (même sur une machine à cœur unique).

Je sais que c’est une très vieille question avec beaucoup de bonnes réponses, mais je suis ici pour souligner quelque chose d’important dans l’environnement actuel:

Si vous souhaitez concevoir une application pour le multithreading, vous ne devez pas concevoir pour un paramètre matériel spécifique. La technologie du processeur progresse assez rapidement depuis des années et le nombre de clients augmente régulièrement. Si vous concevez délibérément votre application de telle sorte qu’elle n’utilise que 4 threads, vous vous restreignez potentiellement dans un système octa-core (par exemple). Maintenant, même les systèmes à 20 cœurs sont disponibles dans le commerce, et une telle conception 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, sans savoir que le code est exécuté avec un autre code.

Le fait est que la grande majorité des programmeurs ne comprennent pas comment concevoir une machine à états. Pouvoir tout placer dans son propre thread libère le programmeur de la nécessité de réfléchir à la manière de représenter efficacement l’état des différents calculs en cours pour pouvoir les interrompre et les reprendre ultérieurement.

À titre d’exemple, considérons la compression vidéo, une tâche très gourmande en processeur. Si vous utilisez un outil d’interface graphique, vous souhaiterez probablement que l’interface rest réactive (affichage de la progression, réponse aux demandes d’annulation, redimensionnement de la fenêtre, etc.). Vous concevez donc votre logiciel d’encodeur pour traiter une grande unité (une ou plusieurs images) à la fois et l’exécuter dans son propre thread, distinct de l’interface utilisateur.

Bien sûr, une fois que vous aurez compris qu’il aurait été intéressant de pouvoir enregistrer l’état de l’encodage en cours afin de fermer le programme pour redémarrer ou jouer à un jeu gourmand en ressources, vous réaliserez que début. Soit ça, soit vous décidez de créer un tout nouveau problème d’hibernation des processus pour que vous puissiez suspendre et reprendre des applications individuelles sur le disque …