La réponse simple est que tous les threads ne s'exécutent pas simultanément. Pour une explication plus complète, lisez la suite.
Le planificateur de tâches du système d'exploitation est généralement conçu pour planifier des applications, ce qui vous permet d'effectuer une tâche pendant que l'ordinateur travaille sur une autre. Autrefois, le test décisif du multitâche formait une disquette tout en faisant autre chose. Si vous vouliez vraiment tester le système d'exploitation, vous formateriez une disquette tout en téléchargeant un fichier via un modem connecté au port série. Comme le matériel est devenu suffisamment puissant pour le faire de manière significative, la lecture vidéo figurait parfois également dans de tels tests. Si le planificateur de tâches du système d'exploitation peut gérer l'exécution de ces tâches sans problème, il peut tout gérer.
Cependant, le planificateur de tâches ne planifie pas réellement les applications (processus), il planifie les threads . Chaque application a au moins un thread, mais peut potentiellement utiliser un grand nombre de threads pour diviser le travail effectué en parties liées ou indépendantes. Par exemple, il est courant pour une application d'avoir un thread qui gère l'interface utilisateur et de créer un autre thread lorsque l'utilisateur lance une opération potentiellement longue (ce qui pourrait être des choses comme l'impression, le recalcul d'une feuille de calcul, un environnement de développement faisant une recherche de symboles, etc. etc.). Certains environnements de programmation introduisent une certaine quantité de threads de manière invisible pour le programmeur; par exemple, Java et .NET peuvent effectuer une récupération de placedans un thread séparé, qui est hors du contrôle immédiat du programmeur. Certains programmes créent un certain nombre de threads au début et les regroupent, car la création de nouveaux threads est une opération relativement coûteuse (vous ne devez donc pas nécessairement avoir à créer un thread à chaque fois que vous en avez besoin). Tout ce qui fait un aperçu est généralement effectué dans un thread séparé, de sorte que le reste de l'interface utilisateur reste réactif pendant la génération de l'aperçu. Etc. Pris ensemble, tout cela signifie que le nombre de threads sur le système à tout moment peut facilement être plusieurs fois le nombre de processus.
Chaque thread peut être dans l'un des quelques états possibles, mais la distinction la plus importante est entre les états en cours d'exécution , exécutable et en attente ; la terminologie peut différer légèrement, mais c'est l'idée générale. À tout moment, un seul thread par processeur virtuel (en raison de l'hyperthreading et de technologies similaires) peut être en cours d'exécution (c'est-à-dire, exécuter des instructions de code machine), mais un nombre illimité de threads peuvent être exécutables (ce qui signifie qu'il est possible d'obtenir la CPU la prochaine fois que le planificateur doit décider quel thread doit être autorisé à s'exécuter). Attendre (également appelés bloqués), les threads ne sont que cela, en attente de quelque chose - les cas les plus courants sont probablement qu'ils attendent des E / S utilisateur, disque ou réseau (l'entrée utilisateur en particulier est exceptionnellement lente).
Le nombre de threads que vous voyez dans le gestionnaire de tâches est le nombre total de threads dans l'un de ces états. Par exemple, le système Windows 7 sur lequel je tape ceci a actuellement environ 70 processus démarrés mais près de 900 threads. Avec tous les processus d'arrière-plan pour gérer diverses tâches et la façon dont ils sont probablement subdivisés en une multitude de threads chacun, ce n'est pas un nombre scandaleux.
Aller un peu plus loin dans les profondeurs de l'implémentation technique, au cœur même du planificateur de tâches d'un système d'exploitation multitâche préventif se trouve généralement une sorte de crochet d'interruption matérielle. Cela signifie que le noyau peut arrêter le CPU lorsqu'il n'a pas de travail utile à effectuer (c'est presque certainement l'une des raisons, sinon la raison, pour laquelle Linux vérifie les HLT
instructions au démarrage sur IA-32-CPU compatibles, et effectue probablement des vérifications similaires sur d'autres architectures), en sachant que, à un moment futur raisonnablement déterminé, une interruption se déclenchera et le planificateur de tâches sera invoqué. Étant donné que l'interruption se déclenche quel que soit le travail effectué par le processeur (c'est l'idée derrière les interruptions), le planificateur est exécuté régulièrement et a la possibilité de déterminer quel thread doit être exécuté pendant la tranche de temps suivante. Étant donné que les commutateurs de contexte sont relativement chers, il est généralement possible (au moins via le code source) de régler l'agressivité avec laquelle le planificateur bascule entre les threads; la commutation des threads rend le système plus réactif, mais la surcharge de commutation signifie que le temps global pour terminer un ensemble de tâches donné est plus long. Le plus rapideLe système sera celui qui bascule entre les threads uniquement lorsque le thread en cours d'exécution n'est plus possible de s'exécuter (ce qui signifie qu'il est bloqué en attente de quelque chose ou qu'il a terminé son travail) car cela minimise la surcharge, tandis que le système le plus réactif basculera entre les threads. chaque fois que le planificateur est appelé car cela minimise le temps moyen d'attente avant qu'un thread particulier n'ait le temps CPU. Le paramètre idéal se situe généralement quelque part entre ces deux, et le compromis entre ces choix est probablement l'une des principales raisons pour lesquelles Linux propose plusieurs programmateurs ainsi que certains paramètres de réglage via la configuration du noyau.
D'un autre côté, les systèmes d'exploitation et les environnements multitâches coopératifs ( Windows 3.x en est un exemple), s'appuient sur chaque application pour céder régulièrement le contrôle au planificateur. Il y a généralement une fonction API spécifiquement conçue pour cela, et souvent de nombreuses fonctions API le feront dans le cadre de leur flux d'exécution interne, car cela aide à rendre l'expérience utilisateur plus fluide. Cette approche de conception fonctionne bien tant que toutes les applications se comportent bien et cèdent le contrôle à de courts intervalles au cours de toutes les opérations de longue durée (longue durée signifiant plus qu'une petite fraction de seconde), mais une application qui ne peut pas se boucher l'ensemble du système. C'est une des principales raisons pour lesquelles Windows 3.x a si mal fait le test multitâche que j'ai mentionné ci-dessus, tandis que OS / 2se promenait gaiement tout en effectuant les mêmes tâches sur le même matériel: une application pouvait dire au lecteur de disquette d'écrire un certain secteur, et le temps qu'il fallait pour le faire avant l'appel renvoyé pouvait en fait être mesurable (des dizaines à des centaines de millisecondes ou plus); un système multitâche préemptif verrait son ordonnanceur s'introduire lors de sa prochaine invocation planifiée, notez que le thread qui est actuellement "en cours d'exécution" est en fait bloqué par l'appel d'écriture et basculez simplement vers un autre thread exécutable. (En pratique, c'est un peu plus impliqué, mais c'est l'idée générale.)
Dans des environnements à la fois préventifs et multitâches, il existe également la possibilité que différents threads aient des priorités différentes. Par exemple, il est probablement plus important d'exécuter en temps opportun le thread qui reçoit des données sur une liaison de communication que celui qui met à jour l'affichage de l'heure système, donc le thread de réception a une priorité élevée et le thread de mise à jour de l'affichage du temps a une priorité faible . Les priorités de thread jouent un rôle dans la décision de l'ordonnanceur de quel thread autoriser l'exécution (par exemple, très simplifié, les threads de haute priorité doivent toujours s'exécuter avant les threads de faible priorité, donc même si le thread de basse priorité a encore du travail à faire, si le thread de haute priorité devient exécutable, il a priorité), mais ces décisions de planification spécifiques n'affectent pas la conception du mécanisme sous-jacent.