

En informatique , un thread d' exécution est la plus petite séquence d'instructions programmées qui peut être gérée indépendamment par un ordonnanceur , qui fait généralement partie du système d'exploitation . Dans de nombreux cas, un thread est un composant d'un processus .
Les différents threads d'un même processus peuvent s'exécuter simultanément (grâce au multithreading), partageant des ressources telles que la mémoire , contrairement à ce qui se passe entre processus différents. Plus précisément, les threads d'un processus partagent son code exécutable ainsi que les valeurs de ses variables allouées dynamiquement et de ses variables globales non locales à un thread à un instant donné.
La mise en œuvre des threads et des processus diffère selon les systèmes d'exploitation.
Les threads sont apparus pour la première fois sous le nom de « tâches » dans le système d'exploitation de traitement par lots d'IBM, OS/360, en 1967. Ce système offrait aux utilisateurs trois configurations possibles du système de contrôle OS/360 , dont la multiprogrammation avec un nombre variable de tâches (MVT). Saltzer (1966) attribue le terme « thread » à Victor A. Vyssotsky .
L' implémentation des threads par Mach a été décrite durant l'été 1986. OS/2 1.0, sorti en 1987, prenait en charge les threads . Le système d'exploitation MP/M de Digital Research offrait le multiprogrammage, le multitâche et le multitâche préemptif pour le processeur Intel 8080 en 1979, puis pour le processeur 8086 en 1981. La première version de Windows à intégrer les threads fut Windows NT , sortie en 1993.
En 1995, l' IEEE a défini l' API pthreads , qui a normalisé une interface pour la programmation multithread portable sur divers systèmes d'exploitation de type Unix . Depuis, pthreads a également été implémenté sous Windows grâce à des bibliothèques tierces telles que pthreads-w32 , qui implémente la norme par-dessus l' API Windows existante .
L’utilisation des threads dans les applications logicielles s’est généralisée au début des années 2000, lorsque les processeurs ont commencé à exploiter plusieurs cœurs. Les applications souhaitant tirer parti de ces cœurs multiples pour améliorer leurs performances ont dû recourir à la concurrence.
Concepts connexes
La planification peut être effectuée au niveau du noyau ou au niveau utilisateur, et le multitâche peut être préemptif ou coopératif . Ceci donne lieu à divers concepts connexes.
Processus
Discussions des utilisateurs
Les fibres constituent une unité d'ordonnancement encore plus légère, à ordonnancement coopératif : une fibre en cours d'exécution doit explicitement céder la place à une autre, ce qui simplifie considérablement leur implémentation par rapport aux threads noyau ou utilisateur . Une fibre peut être exécutée dans n'importe quel thread du même processus. Ceci permet aux applications d'améliorer leurs performances en gérant elles-mêmes leur ordonnancement, au lieu de dépendre de l'ordonnanceur du noyau (qui peut ne pas être optimisé pour l'application). Certaines implémentations de recherche du modèle de programmation parallèle OpenMP utilisent des fibres pour leurs tâches. Les coroutines sont étroitement liées aux fibres , la différence étant que les coroutines sont une construction de niveau langage, tandis que les fibres sont une construction de niveau système.
Threads vs processus
Les threads diffèrent des processus multitâches traditionnels des systèmes d'exploitation à plusieurs égards :
- Les processus sont généralement indépendants, tandis que les threads existent en tant que sous-ensembles d'un processus.
- Les processus contiennent beaucoup plus d'informations d'état que les threads, tandis que plusieurs threads au sein d'un même processus partagent l'état du processus ainsi que la mémoire et d'autres ressources.
- Les processus possèdent des espaces d'adressage distincts , tandis que les threads partagent leur espace d'adressage.
- Les processus interagissent uniquement par le biais de mécanismes de communication interprocessus fournis par le système.
- Le changement de contexte entre les threads d'un même processus est généralement plus rapide que le changement de contexte entre les processus.
Les systèmes tels que Windows NT et OS/2 sont réputés pour leurs threads bon marché et leurs processus coûteux ; dans d'autres systèmes d'exploitation, la différence n'est pas aussi grande, sauf en ce qui concerne le coût d'un changement d'espace d'adressage , qui sur certaines architectures (notamment x86 ) entraîne une vidange du tampon de traduction (TLB).
Les avantages et les inconvénients des threads par rapport aux processus sont les suivants :
- Consommation réduite de ressources grâce aux threads : en utilisant des threads, une application peut fonctionner en utilisant moins de ressources qu’avec plusieurs processus.
- Partage et communication simplifiés des threads : contrairement aux processus, qui nécessitent un mécanisme de passage de messages ou de mémoire partagée pour effectuer une communication interprocessus (IPC), les threads peuvent communiquer via les données, le code et les fichiers qu’ils partagent déjà.
- Un thread peut faire planter un processus : comme les threads partagent le même espace d’adressage, une opération illégale effectuée par un thread peut faire planter l’ensemble du processus ; par conséquent, un thread défaillant peut perturber le traitement de tous les autres threads de l’application.
Planification
Planification préemptive vs planification coopérative
Les systèmes d'exploitation planifient les threads de manière préemptive ou coopérative . Les systèmes d'exploitation multi-utilisateurs privilégient généralement le multithreading préemptif pour son contrôle plus précis du temps d'exécution grâce aux changements de contexte . Cependant, la planification préemptive peut entraîner des changements de contexte imprévus, provoquant ainsi des conflits de verrouillage , des inversions de priorité ou d'autres effets indésirables. À l'inverse, le multithreading coopératif repose sur la capacité des threads à céder le contrôle de l'exécution, garantissant ainsi leur exécution complète . Cela peut poser problème si un thread multitâche coopératif se bloque en attendant une ressource ou s'il prive d'autres threads de ressources en ne cédant pas le contrôle de l'exécution lors de calculs intensifs.
Systèmes monoprocesseurs vs systèmes multiprocesseurs
Jusqu'au début des années 2000, la plupart des ordinateurs de bureau ne disposaient que d'un seul processeur monocœur, sans prise en charge du multithreading matériel . Cependant, le multithreading était encore utilisé sur ces machines car le passage d'un thread à l'autre était généralement plus rapide que les changements de contexte complets . En 2002, Intel a intégré la prise en charge du multithreading simultané au processeur Pentium 4 , sous le nom d' Hyper-Threading ; en 2005, Intel a lancé le processeur bicœur Pentium D et AMD le processeur bicœur Athlon 64 X2 .
Les systèmes monoprocesseurs implémentent généralement le multithreading par répartition temporelle : l’ unité centrale de traitement (CPU) alterne entre différents threads logiciels . Ce changement de contexte est généralement suffisamment fréquent pour que les utilisateurs perçoivent l’exécution des threads ou des tâches comme étant en parallèle (sur les systèmes d’exploitation serveur/poste de travail courants, la durée maximale d’exécution d’un thread, lorsque d’autres threads sont en attente, est souvent limitée à 100–200 ms). Sur un système multiprocesseur ou multicœur , plusieurs threads peuvent s’exécuter en parallèle , chaque processeur ou cœur exécutant un thread distinct simultanément ; sur un processeur ou un cœur doté de threads matériels , des threads logiciels distincts peuvent également être exécutés simultanément par des threads matériels distincts.
Modèles de filetage
1:1 (multithreading au niveau du noyau)
Les threads créés par l'utilisateur, en correspondance un à un avec les entités planifiables du noyau constituent l'implémentation de threads la plus simple. OS/2 et Win32 ont utilisé cette approche dès le départ, tandis que sous Linux, la bibliothèque GNU C l'implémente (via NPTL ou l'ancienne bibliothèque LinuxThreads ). Cette approche est également utilisée par Solaris , NetBSD , FreeBSD , macOS et iOS .
M :1 (multithreading au niveau utilisateur)
Un modèle M :1 implique que tous les threads applicatifs sont mappés sur une seule entité planifiée au niveau du noyau ; le noyau n'a aucune connaissance des threads applicatifs. Grâce à cette approche, les changements de contexte sont très rapides et peuvent être implémentés même sur des noyaux simples ne prenant pas en charge le multithreading. Cependant, l'un des principaux inconvénients est l'impossibilité de tirer parti de l'accélération matérielle sur les processeurs multithreads ou les ordinateurs multiprocesseurs : un seul thread est exécuté simultanément. Par exemple : si l'un des threads doit exécuter une requête d'E/S, le processus entier est bloqué et l'avantage du multithreading ne peut être exploité. GNU Portable Threads utilise le multithreading au niveau utilisateur, tout comme State Threads .
M : N (filetage hybride)
Le modèle M : N associe un certain nombre N :1 »). En général, les systèmes de multithreading « M : N » sont plus complexes à implémenter que les threads noyau ou utilisateur, car ils nécessitent des modifications du code à la fois au niveau noyau et au niveau utilisateur inversion de priorité , ainsi que d'un ordonnancement sous-optimal sans une coordination poussée (et coûteuse) entre l'ordonnanceur utilisateur et l'ordonnanceur noyau.
Exemples de mise en œuvre hybride
- Activations du planificateur utilisées par les anciennes versions de l'implémentation de la bibliothèque de threads POSIX native de NetBSD (un modèle M : N par opposition à un modèle d'implémentation noyau ou espace utilisateur 1:1)
- Processus légers utilisés par les anciennes versions du système d'exploitation Solaris
- Marcel, du projet PM2 .
- Le système d'exploitation pour le Tera- Cray MTA-2
- Le compilateur Glasgow Haskell (GHC) pour le langage Haskell utilise des threads légers qui sont planifiés sur les threads du système d'exploitation.
Historique des modèles de multithreading dans les systèmes Unix
SunOS 4.x implémentait les processus légers (LWP). NetBSD 2.x et versions ultérieures, ainsi que DragonFly BSD, implémentaient les LWP sous forme de threads noyau (modèle 1:1). SunOS 5.2 à 5.8, de même que NetBSD 2 à 4, implémentaient un modèle à deux niveaux, multiplexant un ou plusieurs threads utilisateur sur chaque thread noyau (modèle M:N). SunOS 5.9 et versions ultérieures, ainsi que NetBSD 5, ont abandonné la prise en charge des threads utilisateur, revenant à un modèle 1:1. FreeBSD 5 implémentait le modèle M:N. FreeBSD 6 prenait en charge les deux modèles (1:1 et M:N) ; les utilisateurs pouvaient choisir celui à utiliser pour un programme donné via le fichier `/etc/libmap.conf`. À partir de FreeBSD 7, le modèle 1:1 est devenu le modèle par défaut. FreeBSD 8 ne prend plus en charge le modèle M:N.
Programmes monothread vs programmes multithread
En programmation informatique , le terme « monothread » désigne le traitement d'une instruction à la fois. Dans l'analyse formelle de la sémantique des variables et de l'état du processus, le terme « monothread » peut également désigner le « retour arrière au sein d'un seul thread », une pratique courante en programmation fonctionnelle .
Le multithreading est principalement présent dans les systèmes d'exploitation multitâches. Ce modèle de programmation et d'exécution répandu permet à plusieurs threads de coexister au sein d'un même processus. Ces threads partagent les ressources du processus, mais peuvent s'exécuter indépendamment. Le multithreading offre aux développeurs une abstraction utile de l'exécution concurrente. Il peut également être appliqué à un seul processus pour permettre l'exécution parallèle sur un système multiprocesseur .
Les bibliothèques de multithreading proposent généralement une fonction permettant de créer un nouveau thread, prenant une fonction en paramètre. Un thread concurrent est alors créé, exécutant la fonction passée et s'arrêtant à la fin de son exécution. Ces bibliothèques offrent également des fonctions de synchronisation des données.
Synchronisation des threads et des données
Les threads d'un même processus partagent le même espace d'adressage. Cela permet à du code exécuté simultanément de s'exécuter de manière étroite et d'échanger des données facilement, sans la surcharge ni la complexité d'un IPC (Interface de Processus ). Cependant, lorsqu'elles sont partagées entre les threads, même des structures de données simples deviennent sujettes à des conditions de concurrence si leur mise à jour nécessite plus d'une instruction CPU : deux threads peuvent tenter de mettre à jour la structure de données simultanément et constater qu'elle est modifiée de manière inattendue. Les bogues causés par des conditions de concurrence peuvent être très difficiles à reproduire et à isoler.
Pour éviter cela, les interfaces de programmation d'applications (API) multithread offrent des primitives de synchronisation, comme les mutex, pour verrouiller les structures de données et empêcher les accès concurrents. Sur les systèmes monoprocesseurs, un thread accédant à un mutex verrouillé doit se mettre en veille, ce qui provoque un changement de contexte. Sur les systèmes multiprocesseurs, le thread peut interroger le mutex via un verrouillage par attente active ( spinlock ). Ces deux méthodes peuvent dégrader les performances et contraindre les processeurs des systèmes multiprocesseurs symétriques (SMP) à se disputer le bus mémoire, surtout si la granularité du verrouillage est trop fine.
D'autres API de synchronisation incluent les variables de condition , les sections critiques , les sémaphores et les moniteurs .
Pools de threads
- Cloner (appel système Linux)
- Communication des processus séquentiels
- multitâche informatique
- Multicœur (informatique)
- Multithreading (matériel informatique)
- Algorithme non bloquant
- Inversion de priorité
- Protothreads
- multithreading simultané
- Modèle de pool de fils
- Sécurité du fil
- Bloc d'informations sur le thread Win32