Fil (informatique)

En informatique , le thread [ θɹɛd ] (en anglais thread , 'thread', 'strand') - également appelé support d'activité ou processus léger - désigne un thread d' exécution ou une séquence d'exécution dans l'exécution d'un programme . Un thread fait partie d'un processus .

On distingue deux types de threads :

  1. Les threads au sens étroit , appelés threads du noyau , s'exécutent sous le contrôle du système d'exploitation .
  2. A l'opposé de cela se trouvent les threads utilisateur , que le programme informatique de l'utilisateur doit gérer entièrement par lui-même.

Cet article traite ici du thread dans un sens plus étroit, c'est-à-dire le thread du noyau.

Threads du point de vue du système d'exploitation

Plusieurs threads du noyau en un seul processus (tâche)

Un thread (noyau) est une exécution séquentielle exécutée au sein d'un processus et partage un certain nombre de ressources avec les autres threads existants ( multithreading ) du processus associé :

Historiquement, Friedrich L. Bauer a inventé le terme processus séquentiel pour cela .

Les threads d'un même processus peuvent être affectés à différents processeurs. Chaque thread a son propre contexte de thread :

  • jeu de registres indépendant comprenant un pointeur d'instruction,
  • sa propre pile , mais surtout dans l'espace d'adressage de processus commun.
  • Comme particularité, il peut y avoir des ressources qui peuvent ou ne peuvent être utilisées que par le thread générateur (exemple : thread-local storage , handle de fenêtre).

Les autres ressources sont partagées par tous les threads. L'utilisation partagée des ressources peut également conduire à des conflits. Ceux-ci doivent être résolus par l'utilisation de mécanismes de synchronisation .

Les threads affectés à un même processus utilisant le même espace d'adressage, la communication entre ces threads est d'emblée très simple (cf. avec la communication interprocess pour les processus).

Chaque « fil » est responsable de l'exécution d'une tâche spécifique. Les brins d'exécution des fonctions du programme peuvent ainsi être divisés en unités gérables et des tâches étendues peuvent être réparties sur plusieurs cœurs de processeur.

Dans la plupart des systèmes d'exploitation, un thread peut avoir un état inactif en plus des états actif (en cours d'exécution), prêt et bloqué (en attente) . Dans l'état 'calcul' (= actif = en cours d'exécution) l'exécution des commandes a lieu sur le CPU , dans l'état 'computing ready' (= ready = ready) le thread est arrêté pour permettre à un autre thread de calculer et dans le cas de ' bloqué' (= en attente) le thread attend un événement (généralement qu'un service du système d'exploitation a été terminé / effectué). Un thread à l'état « inactif » est principalement en cours de configuration par le système d'exploitation ou a fini de calculer et peut maintenant être supprimé de la liste des threads par le système d'exploitation ou réutilisé d'une autre manière.

Différence de sens entre thread (noyau) et processus, tâche et thread utilisateur

Un processus décrit l'exécution d'un programme informatique sur un ou plusieurs processeur (s). Un espace d'adressage et d'autres ressources du système d'exploitation sont affectés à un processus - en particulier, les processus sont protégés les uns des autres : si un processus tente d'accéder à des adresses ou des ressources qui ne lui ont pas été affectées (et appartiennent éventuellement à un autre processus), cela échouera et le système d'exploitation l'utilisera annulé. Un processus peut contenir plusieurs threads ou - si un traitement parallèle n'est pas prévu au cours du programme - un seul thread. Les threads partagent des processeurs, de la mémoire et d'autres ressources dépendantes du système d'exploitation telles que des fichiers et des connexions réseau au sein d'un processus. Pour cette raison, l'effort administratif pour les threads est généralement inférieur à celui des processus. Un avantage d'efficacité significatif des threads est, d'une part, que, contrairement aux processus, un changement complet du contexte de processus n'est pas nécessaire lors du changement de threads, puisque tous les threads utilisent une partie commune du contexte de processus, et d'autre part part, dans la communication simple et l'échange de données rapide entre les threads.

Les systèmes d'exploitation dits multitâches existaient déjà dans les années 1980 , car, contrairement aux systèmes orientés métier, notamment dans la technologie informatique de processus connue à l'époque, plusieurs tâches devaient être effectuées en parallèle. A cette époque, le terme tâche était utilisé pour décrire une tâche du point de vue du système d'exploitation, ce qui est synonyme de processus. Le terme tâche (en allemand : tâche ) est aussi couramment utilisé dans l'architecture logicielle de focus pour les tâches connexes, et surtout rarement synonyme de fil utilisé.

Un fil est littéralement un fil d'exécution unique d'un programme, mais le terme fil est utilisé pour le fil d'exécution du point de vue du système d'exploitation (fil de noyau). Dans un logiciel utilisateur, ce volet d'exécution peut être encore subdivisé en volets individuels indépendants au moyen d'une programmation appropriée. Dans le monde anglophone, le terme user thread (pour Microsoft fiber , allemand : fiber ) s'est imposé pour désigner un seul de ces brins d'exécution du logiciel utilisateur . Dans le cas des threads utilisateurs, le logiciel utilisateur est seul responsable de la gestion de ses threads d'exécution.

Exemples

Le tableau suivant montre des exemples des différentes combinaisons de processus, noyau et thread utilisateur :

traiter
Fil de noyau

Fil d' utilisateur
Exemple
non non non Un programme informatique fonctionnant sous MS-DOS . Le programme ne peut effectuer qu'une des trois actions à la fois.
non non Oui Windows 3.1 à la surface du DOS. Tous les programmes Windows s'exécutent dans un processus simple, un programme peut détruire la mémoire d'un autre programme, mais cela est remarqué et une erreur de protection générale ( GPF a) en résulte.
non Oui non Implémentation originale du système d'exploitation Amiga . Le système d'exploitation prend entièrement en charge les threads et permet à plusieurs applications de s'exécuter indépendamment les unes des autres, planifiées par le noyau du système d'exploitation. En raison du manque de support de processus, le système est plus efficace (car il évite les dépenses supplémentaires de protection de la mémoire), au prix que des erreurs d'application peuvent paralyser l'ensemble de l'ordinateur.
non Oui Oui DR-DOS 7.01, 7.03, 8.0 ; DR-DOS amélioré toutes les versions
Mac OS 9 prend en charge les threads utilisateur à l'aide du gestionnaire de threads d'Apple et les threads du noyau à l'aide des services de multitraitement d'Apple , qui fonctionnent avec le nanokernel , introduit dans Mac OS 8.6. Cela signifie que les threads sont pris en charge, mais la méthode MultiFinder est toujours utilisée pour gérer les applications.
Oui non non Implémentations les plus connues d' Unix (sauf Linux). Le système d'exploitation peut exécuter plus d'un programme à la fois, les exécutions du programme sont protégées les unes contre les autres. Lorsqu'un programme se comporte mal, il peut perturber son propre processus, ce qui peut entraîner l'arrêt de ce processus sans perturber le système d'exploitation ou d'autres processus. Cependant, l'échange d'informations entre les processus peut être sujet à des erreurs (lors de l'utilisation de techniques telles que la mémoire partagée ) ou complexe (lors de l'utilisation de techniques telles que la transmission de messages ). L'exécution asynchrone des tâches nécessite un appel système fork () complexe .
Oui non Oui Sun OS (Solaris) de Sun. Sun OS est la version d' Unix de Sun Microsystems . Sun OS implémente les threads utilisateurs comme des threads dits verts pour permettre à un processus simple d'exécuter plusieurs tâches de manière asynchrone, par exemple jouer un son, repeindre une fenêtre , ou réagir à un événement opérateur tel que la sélection du bouton d'arrêt . Bien que les processus soient gérés de manière préventive , les fils verts fonctionnent en coopération. Ce modèle est souvent utilisé à la place des vrais threads et est toujours à jour dans les microcontrôleurs et les dispositifs dits embarqués , et est utilisé très fréquemment.

Windows 3.x en mode amélioré lors de l'utilisation de boîtes DOS entre également dans cette catégorie, car les boîtes DOS représentent des processus indépendants avec un espace d'adressage séparé.

Oui Oui non C'est le cas général des applications sous Windows NT à partir de 3.51 SP3+, Windows 2000, Windows XP, Mac OS X, Linux, et autres systèmes d'exploitation modernes. Tous ces systèmes d'exploitation permettent au programmeur d'utiliser des threads utilisateur ou des bibliothèques que possèdent les threads utilisateur, mais n'utilisent pas tous les programmes de cette possibilité. De plus, des threads utilisateurs peuvent également être créés automatiquement par le système d'exploitation pour chaque application démarrée (par exemple pour faire fonctionner l'interface utilisateur graphique) sans que le programmeur n'ait à le faire explicitement ; ces programmes sont alors automatiquement multithreadés . Il est également nécessaire d'utiliser plusieurs threads utilisateurs si vous souhaitez utiliser plusieurs processeurs/cœurs de processeur dans l'application.
Oui Oui Oui La plupart des systèmes d'exploitation depuis 1995 entrent dans cette catégorie. Utilisation de threads pour exécuter simultanément est le choix commun, bien que multi-processus et multi-fibres applications existent également. Ceux-ci sont utilisés, par exemple, pour qu'un programme puisse traiter son interface utilisateur graphique pendant qu'il attend une entrée de l'utilisateur ou qu'il effectue un autre travail en arrière-plan.

Remarques:

  • L'utilisation des threads utilisateur est en principe indépendante du système d'exploitation. C'est donc possible avec n'importe quel système d'exploitation. Il est seulement important que l'état complet du processeur puisse être lu et réécrit (les threads utilisateur ont également été implémentés dans certains systèmes d'exploitation 8 bits, par exemple en tant que GEOS sur le C64 / C128). Par conséquent, les valeurs du tableau doivent être considérées comme des valeurs de référence.
  • Certains systèmes d'exploitation modernes (par exemple Linux) ne permettent plus une distinction stricte entre les processus et les threads du noyau. Les deux se font avec le même appel système (clone (2)) ; vous pouvez spécifier de manière fine quelles ressources doivent être partagées et lesquelles ne le sont pas (exception : registre CPU, pile). Avec un certain nombre de ressources, cela peut même être modifié pendant que le thread est en cours d'exécution (mémoire : TLS vs. mémoire partagée, descripteur de fichier : socketpair).

Implémentations

Java

Travailler avec plusieurs threads est prévu dès le départ en Java . Le multithreading fonctionne également si le système d'exploitation ne le prend pas en charge ou ne le supporte que de manière inadéquate. Ceci est possible car la machine virtuelle Java peut prendre en charge la commutation des threads, y compris la gestion de la pile. Dans les systèmes d'exploitation avec prise en charge des threads, les propriétés du système d'exploitation peuvent être utilisées directement. La décision à ce sujet réside dans la programmation de la machine virtuelle.

En Java, il existe la classe Thread dans le package de base java.lang . Les instances de cette classe sont des unités administratives des threads. Thread peut soit être utilisé comme classe de base pour une classe d'utilisateurs, soit une instance de Thread connaît une instance de n'importe quelle classe d'utilisateurs. Dans le second cas, la classe utilisateur doit implémenter l'interface java.lang.Runnable et donc contenir une méthode run() .

Un thread est démarré en appelant thread.start() . La méthode run() affectée est traitée. Tant que run() est en cours d'exécution, le thread est actif.

Dans la méthode run() ou dans les méthodes appelées à partir de là, l'utilisateur peut utiliser wait() pour laisser le thread attendre pendant une période de temps (spécifiée en millisecondes) ou pour n'importe quelle durée. Cette attente se termine par une notification () d'un autre thread. Il s'agit d'un mécanisme important pour la communication inter-thread. wait() et notify() sont des méthodes de la classe Object et peuvent être utilisées sur toutes les instances de données. Les wait() et notify() associés doivent être organisés dans la même instance (une classe d'utilisateurs) ; il est logique de transférer les données qu'un thread souhaite communiquer à l'autre dans cette instance.

La réalisation des sections critiques se fait avec des synchros .

Dans la première version de Java, des méthodes de la classe Thread ont été introduites pour interrompre un thread de l'extérieur, continuer et abandonner : suspend() , resume() et stop() . Cependant, ces méthodes ont rapidement été qualifiées d' obsolètes dans les versions ultérieures . Dans l'explication détaillée, il a été indiqué qu'un système n'est pas sûr si un thread peut être arrêté ou avorté de l'extérieur. La raison, en quelques mots, est la suivante : un thread peut être dans une phase d'une section critique et certaines données peuvent avoir changé. S'il est arrêté, la section critique est bloquée et des interblocages en résultent. S'il est annulé et que le blocage est levé par le système, alors les données sont incohérentes. À ce stade, un système d'exécution ne peut pas prendre sa propre décision ; seul le programme utilisateur lui-même peut contrôler un thread en cours d'arrêt ou d'abandon.

.RAPPORTER

.NET prend en charge nativement la programmation de threads. Ceci est implémenté par les classes de l'espace de noms System.Threading .

En plus des constructions de processus et de threads mentionnées ci-dessus, il existe également le concept de domaine d' application ( AppDomain ). Un processus peut contenir plusieurs domaines d'application, ceux-ci sont isolés du runtime ("processus logique"), une ressource fournie par le framework .Net est liée au domaine d'application générateur. Les ressources du système d'exploitation sous-jacent (y compris les threads du noyau !) ne sont pas liées à ces limites de processus logiques.

Le runtime .NET propose également un pool de threads géré par le runtime, qui est utilisé par le runtime pour traiter les événements asynchrones et les opérations d'entrée/sortie.

Le runtime .NET fait également la distinction entre les threads de premier plan et les threads d'arrière-plan. Un thread devient le thread d'arrière-plan en définissant la propriété Background sur true . Un processus se termine lorsque le dernier thread de premier plan est terminé. Tous les threads d'arrière-plan en cours d'exécution sont automatiquement terminés. Les threads du pool de threads sont démarrés en tant que threads d'arrière-plan.

Un thread indépendant est démarré via une nouvelle instance d'une classe de thread, à laquelle une fonction de rappel ( délégué ) est passée dans le constructeur . Le thread est ensuite démarré à l'aide de la méthode d'instance Start() . Le thread se termine lorsque la fonction de rappel rend le contrôle à l'appelant.

Alternativement, le pool de threads du runtime .NET peut être utilisé pour un bref traitement en arrière-plan. Cela contient un certain nombre de threads qui peuvent être utilisés pour le traitement via ThreadPool.QueueUserWorkItem () . Après le retour de la fonction de rappel, le thread n'est pas détruit par le système d'exploitation, mais est mis en cache pour une utilisation ultérieure. L'avantage de cette classe est l'utilisation optimisée et limitée de l'équipement sous-jacent.

Le contrôle externe des threads est possible ( Abort () , Suspend () , Resume () ), mais peut conduire à des événements imprévisibles tels que des interblocages ou des abandons du domaine d'application. Par conséquent, Suspendre et Reprendre sont marqués comme obsolètes dans les nouvelles versions de .NET.

Les threads sont synchronisés avec un WaitHandle . Ceci est principalement utilisé via la classe Monitor , qui utilise un mutex mis à disposition par chaque objet .NET . En C #, vous pouvez utiliser le verrou (objet) {instruction; } La construction peut être utilisée. De nombreuses classes du .Net Framework existent également dans une variante thread-safe qui peut être créée à l'aide d'une méthode statique Synchronized () .

Unix/Linux

Sous Unix, il y a toujours eu des appels système faciles à utiliser pour créer des processus parallèles ( fork ). C'est la manière traditionnelle d'implémenter le traitement parallèle sous Unix / Linux. Des threads ont été ajoutés dans les versions ultérieures d'Unix, mais la portabilité entre les versions antérieures n'était pas garantie. Le thread POSIX standard ( Native POSIX Thread Library ) a finalement stipulé une gamme minimale uniforme de fonctions et une API uniforme , qui est également prise en charge par les versions Linux actuelles ( NPTL ). Par rapport à un processus, un thread est également appelé processus léger ( Solaris ), car la commutation entre les processus nécessite plus d'efforts (temps de calcul) dans le système d'exploitation que la commutation entre les threads d'un processus.

les fenêtres

Afin de créer votre propre thread en C ou C++ sous Windows, vous pouvez accéder directement aux interfaces API Windows. Pour ce faire, vous devez appeler comme un modèle simple :

#include <windows.h>
DWORD threadId;
HANDLE hThread = CreateThread (NULL, 0, runInThread, p, 0, &threadId);
CloseHandle (hThread);

runInThreadest le sous - programme qui doit s'exécuter dans ce thread, il est appelé immédiatement après. S'il est runInThreadterminé, le thread est également terminé, comme Thread.run()en Java.

Cette API est une interface orientée C. Afin de programmer les threads de manière orientée objet, runInThreadune méthode d'une classe peut être appelée dans le sous-programme selon le schéma suivant :

DWORD WINAPI runInThread(LPVOID runnableInstance)
{
   Runnable* runnable = static_cast <Runnable*> (runnableInstance);
                        // Klassenzeiger oder Zeiger auf Basisklasse
   return(runnable->run()); // run-Methode dieser Klasse wird gerufen.
}

La classe qui contient la méthode run() pour le thread est Runnablecontenue dans une classe ici , qui peut également être une classe de base d'une classe plus grande. Le pointeur vers l'instance d'une Runnableclasse qui peut être dérivée doit être passé en paramètre (p) à CreateThread, à savoir transtypé en (Runnable *). Vous disposez donc de la même technologie qu'avec Java. La classe de base universelle (une interface) pour toutes les classes dont les méthodes run() doivent s'exécuter dans un thread séparé est définie comme suit :

class Runnable // abstrakte Basisklasse (als Schnittstelle verwendbar)
   {
      virtual DWORD fachMethode()=0; // API zum Vererben
   public:
      DWORD run() { return(fachMethode()); } // API zum Aufrufen
      virtual ~Runnable() {} // Wenn vererbt werden soll: Dtor virtuell
   };

La classe d'utilisateurs avec la méthode spécialiste est définie ci-dessous :

class MyThreadClass : public Runnable
   {
      DWORD fachMethode(); // Überschreibt/implementiert die Fachmethode
   };

La classe d'utilisateurs est alors instanciée et le thread démarré :

MyThreadClass myThreadObject;
hThread = CreateThread (NULL, 0, runInThread, &myThreadObject, 0, &threadId);

En raison de la liaison dynamique , la méthode souhaitée est myThread->fachMethode()appelée. Attention : Le cycle de vie de myThreadObject doit être respecté : Vous ne devez pas l'"effacer" implicitement tant que le nouveau thread fonctionne encore avec ! La synchronisation des threads est requise ici.

D'autres accès au thread au niveau de l'API peuvent être effectués avec la connaissance du HANDLE retourné, par exemple

SetThreadPriority (hThread, THREAD_PRIORITY_BELOW_NORMAL);

ou pour runInThreadinterroger la valeur de retour de la méthode appelée (dans l'exemple 0) :

DWORD dwExitCode;
GetExitCodeThread (hThread, &dwExitCode);

difficulté

L'utilisation de threads et de mécanismes de synchronisation simples tels que les mutex et les sémaphores s'est avérée exigeante en pratique. Étant donné que le flux du programme n'est plus simplement séquentiel, il est difficile pour un développeur de le prévoir. Étant donné que la séquence d'exécution et le changement entre les threads sont régulés par le planificateur et que le développeur a peu d'influence sur cela, un programme concurrent peut facilement se retrouver dans un état global auparavant inattendu, qui se manifeste par des interblocages , des verrous en direct , des erreurs de données et des plantages. . Ces effets se produisent sporadiquement et sont donc difficilement reproductibles, ce qui rend difficile le dépannage dans une application.

Représentation des threads en UML

Dans le langage de modélisation unifié (UML), les processus parallèles sont souvent représentés par des diagrammes d'état . Dans un diagramme d'états, des diagrammes d'états partiels parallèles internes peuvent être représentés dans un état. Tous les diagrammes d'état du système global sont traités quasi-parallèlement. Le quasi-parallélisme est obtenu par le fait que chaque transition d'état est très courte (en pratique quelques microsecondes à quelques millisecondes) et donc les traitements successifs semblent être parallèles. La transition d'un état à un autre est généralement déclenchée par un événement qui a été précédemment écrit dans la soi-disant file d'attente d'événements. Selon la définition donnée ci-dessus, cette transition due à un événement est un thread utilisateur. En principe, le parallélisme ainsi implémenté peut être réalisé avec un seul thread du système d'exploitation.

Si UML est utilisé pour des systèmes rapides, alors la question de la priorisation temporelle joue un rôle. Si les transitions d'état peuvent prendre beaucoup de temps ou si une transition doit également attendre des conditions (cela se produit déjà lors de la lecture ou de l'écriture dans un fichier), alors le parallélisme avec les threads doit être implémenté. Pour cette raison, le traitement du diagramme d'état doit pouvoir être affecté à plusieurs threads du système, qui peuvent avoir des priorités différentes. L'outil UML Rhapsody connaît le terme classe active pour cela . Chaque classe active se voit attribuer son propre thread.

En plus de la formulation de travaux parallèles avec des diagrammes d'états, le parallélisme avec des threads peut également être modélisé dans des systèmes conçus par UML. Le modèle de programmation proposé par Java peut être utilisé pour cela. Dans ce cas, une classe Thread explicite avec les propriétés connues en Java doit être incluse dans le modèle utilisateur. Cela permet de maîtriser plus facilement et plus efficacement les problèmes très cycliques, comme le montre l'exemple suivant :

void run()
{
   while (not_abort)           // zyklisch bis zum Abbruch von außen
   {
      data.wait();             // der Zyklus beginnt, wenn Daten vorliegen
      dosomething();           // Abarbeitung mehrerer Dinge
      if (condition)
      {
         doTheRightThing();    // Abarbeitung ist von Bedingungen abhängig
         partnerdata.notify(); // andere Threads benachrichtigen
      }
   }
}

La méthode run() montrée ici est une méthode d'une classe d'utilisateurs, dans laquelle tout le traitement dans le thread est décrit dans les lignes de programme, comme c'est d'habitude avec le traitement fonctionnel dans l'UML. L'UML permet de montrer à cette classe d'utilisateurs, la classe Thread associée et leurs relations ( class diagram ), complétée, par exemple, par des diagrammes de séquence . La programmation est claire. Un diagramme d'état n'offre pas de meilleures options graphiques dans ce cas.

Voir également

Littérature

liens web

Preuve individuelle

  1. voir gotw.ca