TP 8 - Fils d'Execution sous Linux

Documents d'aide

Agréable à lire

  • TLPI - Chapitre 29, Fil: Introduction
  • TLPI - Chapitre 30, Threads: Synchronisation des threads
  • TLPI - Chapitre 31, Filetage: Sécurité des filets et stockage par fil

Présentation théorique

Dans les laboratoires précédents, le concept process était présenté, il s'agissait de la principale unité d'allocation de ressources pour les utilisateurs. Dans ce laboratoire, le concept de thread (ou thread ) est présenté, il s'agit de l'unité de planification élémentaire d'un système. En plus des processus, les threads d'exécution constituent un mécanisme par lequel un ordinateur peut exécuter plusieurs tâches simultanément.

Un thread d'exécution existe dans un processus et c'est une unité plus fine qu'elle ne l'est. Lorsqu'un processus est créé, il ne contient qu'un seul thread qui exécute le programme séquentiel. Ce fil peut à son tour créer d'autres threads; ces threads exécuteront des portions du binaire associé au processus en cours, éventuellement identiques au thread d'origine (qui les a créées).

Différences entre les threads d'exécution et les processus

  • Les processus ne partagent pas de ressources entre eux (à moins que le programmeur utilise un mécanisme spécial pour cela, la mémoire partagée par exemple), tandis que les threads partagent la majorité des ressources d'un processus par défaut. Changer une telle ressource depuis un thread est instantanément visible depuis les autres threads:
    • segments de mémoire tels que '.heap', '.data' et '.bss' '(donc les variables qui y sont stockées)
    • descripteurs de fichier (la fermeture d'un fichier est immédiatement visible pour tous les threads) quel que soit le type de fichier:
      • prises;
      • fichiers normaux
      • Nommé tuyau
      • Les fichiers représentant les périphériques matériels (par exemple, / dev / sda1 ).
  • Chaque thread a son propre contexte d'exécution, composé de:
    • pile
    • ensemble de registres (donc un programme - registre (E) IP )

Les processus sont utilisés par le responsable de sécurité pour regrouper et allouer des ressources, et les threads pour planifier l'exécution du code qui accède à ces ressources (partagées).

Avantages des threads

Étant donné que tous les threads d'un processus utilisent l'espace d'adressage du processus auquel ils appartiennent, leur utilisation présente de nombreux avantages:

  • Créer / détruire un fil prend moins de temps que créer / détruire un processus
  • La longueur du contexte de commutation entre les threads du même processus est très petite, car il n'est pas nécessaire de “changer” l'espace d'adressage (pour plus d'informations, recherchez “TLB flush”)
  • la communication entre les threads a une surcharge moins importante (réalisée en modifiant certaines zones de mémoire dans l'espace d'adressage partagé)

Les threads d'exécution peuvent être utiles dans de nombreuses situations, par exemple pour améliorer le temps de réponse des applications à interface graphique, où un traitement intensif du processeur est généralement effectué dans un thread différent de celui affichant l'interface. .

Ils simplifient également la structure d'un programme et permettent d'utiliser moins de ressources (car différentes formes d'IPC ne sont pas nécessaires pour communiquer).

Types de fils d'execution

Du point de vue de la mise en œuvre, il existe 3 catégories de threads:

  • Threads au niveau du noyau (KLT)
  • Threads de niveau utilisateur (ULT)
  • Fils hybrides

Détails des types de fil

Détails des types de fil

Threads au niveau du noyau

La gestion des threads et la planification se font dans le noyau; les programmes créent / détruisent les fils d'exécution par le biais d'appels système. Le noyau conserve les informations de contexte pour les processus et les threads de processus, et la planification de l'exécution est basée sur les threads.

Avantaje:

  • Si nous avons plusieurs processeurs, nous pouvons exécuter simultanément plusieurs threads du même processus.
  • bloquer un thread ne signifie pas bloquer l'ensemble du processus;
  • nous pouvons écrire du code noyau basé sur des threads.

Desavantaje:

  • la commutation de contexte est effectuée par le noyau (avec une vitesse de commutation inférieure):
    • passe d'un thread de noyau
    • Le noyau retourne le contrôle d'un autre thread.

Threads de niveau utilisateur

Le noyau n'est pas au courant de l'existence des threads et leur gestion est assurée par le processus dans lequel ils existent (la mise en œuvre de la gestion des threads est généralement effectuée dans des bibliothèques). Changer le contexte ne nécessite pas d'intervention du noyau et l'algorithme de planification dépend de l'application.

Avantaje:

  • Le changement de contexte n'implique pas le noyau ⇒ changement rapide
  • la planification peut être choisie par l'application; l'application peut utiliser cette planification d'amélioration des performances
  • Les threads d'exécution peuvent s'exécuter sur n'importe quel SO, y compris les SO qui ne prennent pas en charge les threads du noyau (ils n'ont besoin que de la bibliothèque qui implémente les threads générés par l'utilisateur).

Desavantaje:

  • Le noyau ne connaît pas les threads ⇒ Si un thread exécute un appel bloquant, tous les threads d'exécution planifiés par l'application seront bloqués. Cela peut constituer un obstacle, car la plupart des appels système sont bloquants. Une solution consiste à utiliser des variantes non bloquantes pour les appels système.
  • Vous ne pouvez pas tirer le meilleur parti des ressources matérielles: le noyau planifie les threads qu'il connaît, un par processeur. Le noyau n'est pas conscient de l'existence du niveau utilisateur ⇒ les threads ne verront qu'un seul thread ⇒ il planifiera le processus pour un maximum d'un processeur, même si l'application aurait plusieurs threads planaires en même temps.

Fils de construction hybrides

Ces threads tentent de combiner les avantages des threads de niveau utilisateur avec ceux des threads de niveau noyau. Une solution consiste à utiliser des câbles au niveau du noyau pour multiplexer des threads au niveau utilisateur. Les KLT sont les unités de base pouvant être réparties sur les processeurs. En règle générale, la création de threads se fait dans l'espace utilisateur, ce qui représente presque toute la planification et la synchronisation. Le noyau ne connaît que les KLT sur lesquels les ULT sont multiplexés et il ne les planifie que. Le planificateur peut éventuellement changer le numéro KLT attribué à un processus.

Le module threading

En ce qui concerne les threads, POSIX ne spécifie pas s'ils doivent être implémentés dans l'espace utilisateur ou dans l'espace noyau. Linux les implémente dans l'espace noyau, mais ne différencie pas les threads de processus, sauf que les threads partagent l'espace d'adressage (les threads d'exécution et les processus constituent un cas particulier de “tâche”). Pour utiliser les threads en Python, nous devons inclure le module threading.

Création des fils d'execution

Le module threading expose la classe Thread. De cette maniere, un fil d'execution est créé lorsqu'on initialise la classe:

import threading
threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
  • group - utilisé pour étendre la classe Thread; on recommande de garder la valeur None
  • target - la fonction qui sera appelée au moment d'exécution du thread
  • name - nom du thread; s'il n'est pas spécifié, on va générer un nom de la forme Thread-N, ou N est un numéro
  • args - la liste des arguments qui sera envoyée a la fonction target;
  • kwargs - un dictionnaire qui va sauvegarder les parametres de la fonction target, sous la forme nom-valeur;
  • daemon - spécifie si le thread est de type daemon; si la valeur est None, la propriété sera héritée du thread courant

Le nouveau fil créé peut etre lancé en exécution en appelant la fonction start(). Il va exécuter le code spécifié par la fonction target a laquelle on va passer les arguments de args ou kwargs.

Pour déterminer le fil d'exécution courant, on peut utiliser la fonction current_thread:

import threading
threading.current_thread()

Attendre les fils d'exécution

Les threads d'exécution sont attendus à l'aide de la fonction pthread_join:

t = threading.Thread()
t.join(timeout=None)

Une fois qu'un thread a exécuté join sur un autre, celui-ci sera bloqué jusqu'a ce que le thread sur lequel on a fait join va terminer son exécution. Si le thread sur lequel on a fait join lancera une excéption du type excepthook sera lancée dans le thread qui a fait join.

On vous conseille d'appeler toujours la fonction join dans le programme principal pour les threads qu'elle génere.

Fils d'exécution de type daemon

Un thread de type daemon a comme but de processer certaines opérations sans avoir un impact sur le fil d'exécution principal.

La propriété principale de ces threads est que le programme principal va finir son exécution s'il ne contient que des threads de ce type.

Meme si elle est une opération permise, on vous recommande de ne pas utiliser la fonction join sur un thread de type daemon.

Terminer les fils d'exécution

Un thread finit son exécution automatiquement, a la fin du code du fil d'exécution.

Interagir avec les fils d'exécution

Pour changer des informations entre le programme principal et d'autres threads, on va utiliser le module queue. A l'aide de ce module, on peut implémenter une queue ou on va placer les messages provenus de la part des fils d'exécution.

Le module supporte la création de 6 types différentes de queues:

  • Queue - type FIFO, le premier élément inseré dans la queue sera le premier extrait
  • LifoQueue - le dernier élément inseré sera le premier extrait
  • PriorityQueue - Chaque élément a une priorité qui détermine quel élément sera extrait
  • SimpleQueue - type FIFO avec des fonctionnalités limitées
  • Empty - on lance une excéption si on essaie d'extraire un élément lorsque la queue est vide
  • Full - on lance une excéption si on essaie d'extraire un élément lorsque la queue est pleine

En ce qui suit on va discuter seulement sur le premiers 3 types de queues. Pour plus de détails concernant les autres classes du module queue, vous pouvez lire la documentation.

Pour créer un queue, on doit instancier l'une des classes du module queue:

import queue
 
fifo_queue = queue.Queue()
lifo_queue = queue.LifoQueue()
priority_queue = queue.PriorityQueue()

Les fonctions put et put_nowait ajoutent des éléments dans la queue:

q.put (item, block=True, timeout=None)
q.put_nowait (item)

Les fonctions get et get_nowait sortent des éléments de la queue:

q.get (block=True, timeout=None)
q.get_nowait ()

Si la queue est vide a l'appel de l'une de ces 2 fonctions, on va lancer une exception de type Empty

Exemple

On va créer un programme qui génere un thread qui recoit des valeurs du programme principal et qu'y répond:

import queue
import threading
 
def worker ():
    while True:
        item = q1.get()
        if item == 'Hello':
            print (item)
            q2.put ('SDE')
        elif item == 'Good':
            print (item)
            q2.put ('bye')
        else:
            break
 
q1 = queue.Queue()
q2 = queue.Queue()
 
t = threading.Thread (target=worker)
t.start ()
 
q1.put ('Hello')
print (q2.get ())
 
q1.put ('Good')
print (q2.get ())
 
q1.put ('exit')
 
t.join()

Données spécifiques aux threads (TSD)

Parfois, il est utile qu'une variable soit spécifique à un thread d'exécution (invisible pour les autres threads). Linux permet aux paires d'être stockées (clé, valeur) dans une zone spécialement désignée sur la pile de chaque thread du processus en cours. La clé a le même rôle que le nom d'une variable: elle désigne l'emplacement de la mémoire où se trouve la valeur.

Chaque thread aura sa propre copie d'une “variable” correspondant à une clé k , qu'il pourra modifier sans que cela soit remarqué par les autres fils ou nécessitant une synchronisation. Par conséquent, TSD est parfois utilisé pour optimiser les opérations nécessitant beaucoup de synchronisation entre les threads: chaque thread calcule les informations spécifiques et une seule étape de synchronisation est nécessaire à la fin pour fusionner les résultats de tous les threads.

Les clés sont pthread_key_t et les valeurs qui leur sont associées sont du type générique void * (pointeurs vers l'emplacement de la pile où la variable est stockée). Nous décrivons maintenant les opérations disponibles avec les variables dans TSD:

Création d'une variable

Une variable est créée en utilisant la classe https://docs.python.org/3/library/threading.html#threading.local"local du module threading.

import threading
 
local_data = threading.local()
local_data.my_var = 'SDE'

Exercices de laboratoire

Exercice 1

  • Crées un programme qui démarre 5 fils d'exécutions. Chaque fil affiche les numéros de 0 a 5.
  • Affichez l'identifiant de chaque fil d'exécution (hint: ident).

Exercice 2

Créez un programme qui recoit 3 numeros comme parametres de la ligne de commande et qui lance en exécution 3 threads. Chaque thread recevra comme parametre du programme principal l'un des 3 numéros et qui affiche sur l'écran pair our impair, en fonction de la parité du numéro.

Exercice 3

Dans le fichier tu directoire 3-numbers, démarrez un fils d'exécution qui affiche les numéros de 0 a 5 à l'intervalle d'une seconde.

N'utilisez pas la fonction join.

Observez le moment ou le processus finit son exécution. Modifiez le programme principal pour que le thread finisse don exécution immédiatement apres avoir affiché les 2 lignes

main 1
main 2

Exercice 4

Dans le fichier du directoire 4-alive, lancez en exécution un thread pour chaque fonction worker définie et attendes seulement les threads toujours en execution (hint: is_alive).

Exercice 5

Créez 2 fils d'exécution, l'un qui va générer les numéros primes de 0 a 50 et l'autre qui va générer les numéros carrés parfaits de 0 a 50. Le programme principal va afficher les numéros générées par les 2 threads et il va s'arrêter.

Exercice 6

Créez un programme qui va recevoir des commande bash du clavier et qui va les exécuter dans un thread.

Exercice 7

Créez un programme qui utilise 3 threads pour trouver le maximum dans une chaîne de longueur 100 000. Chaque thread lit les valeurs de chaîne dans une queue et place le maximum dans une autre. Utilisez la commande queue.join pour la synchronisation.

sde/laboratoare/08.txt · Last modified: 2020/04/08 13:22 by ioana_maria.culic
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0