This is an old revision of the document!


TP 8 - Fils d'Execution

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 discussions

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.

Support POSIX

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 sous Linux, nous devons inclure l'en-tête pthread.h (où les déclarations de fonctions et les types de données sont obligatoires) et utiliser la bibliothèque libpthread .

Création des discussions

Un fil est créé en utilisant pthread_create:

int pthread_create(pthread_t *tid, const pthread_attr_t *tattr, 
                   void*(*start_routine)(void *), void *arg);

Le nouveau thread créé aura l'identifiant tid et s'exécutera en même temps que le thread à partir duquel il a été créé. Il exécutera le code spécifié par la fonction start_routine , qui sera suivi de l'argument arg . Si la fonction exécutée nécessite plusieurs paramètres, ceux-ci peuvent être regroupés dans une structure, dans le champ arg , en plaçant un pointeur sur cette structure.

L'attribut tattr définit les attributs du nouveau thread. Si nous passons la valeur NULL , le thread sera créé avec les attributs par défaut.

Vous pouvez utiliser la fonction pthread_self pour déterminer l'identificateur de thread actuel:

pthread_t pthread_self(void);

En attente des discussions

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

int pthread_join(pthread_t th, void **thread_return);

Le premier paramètre spécifie l'identificateur de thread attendu, et le second spécifie l'emplacement de la valeur renvoyée par la fonction enfant (via pthread_exit ou via -a retour de la routine utilisée dans pthread_create).

Les threads d'exécution appartiennent à deux catégories: unifiable et détachable .

Détails des fils unificateurs et détachables

Détails des fils unificateurs et détachables

  • unifiable :
    • Autoriser l'unification avec d'autres exécutables qui s'appellent pthread_join.
    • Les ressources remplies de threads ne sont pas publiées immédiatement après la fin du thread, mais sont conservées jusqu'à ce qu'un autre thread exécute pthread_join (Process zombie)
    • implicitement les fils sont unifiables

* Amovible

  • un fil est détachable si:
    • a été créé détachable.
    • Cet attribut a été modifié lors de l'exécution par pthread_detach.
  • ne peut pas exécuter un pthread_join sur eux
  • libérera les ressources dès qu'elles auront fini (analogie avec ignorer le signal SIGCHLD dans le parent à la fin de l'exécution du processus enfant)

Finition des threads

Un thread exécute son exécution:

  • lors d'un appel de fonction pthread_exit:
    void pthread_exit(void *retval);
  • automatiquement à la fin du code du thread.

Le paramètre “retval” indique au parent un message sur la manière de mettre fin à l'enfant. pthread_join prendra en charge cette valeur.

Les méthodes comme un fil pour en terminer un autre sont:

  • Établir un protocole de fin (par exemple, le thread définit une variable globale que l'esclave vérifie périodiquement).
  • Le mécanisme thread annulation fourni par libpthread. Cependant, cette méthode n'est pas recommandée car elle est lourde et pose des problèmes de nettoyage très délicats. Pour plus de détails: Terminaison de thread

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 et suppression d'une variable

Une variable est créée en utilisant pthread_key_create:

int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *));

Le deuxième paramètre est une fonction de nettoyage. Il peut avoir l'une des valeurs suivantes:

  • NULL et est ignoré

* pointeur sur une fonction de nettoyage exécutée à la fin du thread

Pour supprimer une variable, appelez pthread_key_delete:

int pthread_key_delete(pthread_key_t key);

La fonction n'appelle pas le nettoyage associé à la variable.

Changer et lire une variable

Après avoir créé la clé, chaque thread peut modifier sa propre copie de la variable associée à l’aide de la fonction pthread_setspecific:

int pthread_setspecific(pthread_key_t key, const void *pointer);

Pour déterminer la valeur d'une variable de type TSD, utilisez la fonction pthread_getspecific:

void* pthread_getspecific(pthread_key_t key);

Fonctions de nettoyage

Les fonctions de nettoyage associées aux TSD peuvent être très utiles pour garantir que les ressources sont libérées lorsqu'un thread se termine seul ou est terminé par un autre thread. Parfois, il peut être utile de pouvoir spécifier de telles fonctions sans nécessairement créer un TSD. Pour cela, il existe des fonctions de nettoyage.

Détails des fonctions de nettoyage

Détails des fonctions de nettoyage

Une telle fonction de nettoyage est une fonction appelée à la fin d'un thread d'exécution. Il reçoit un seul paramètre du type 'void *' qui est spécifié lors de l'enregistrement de la fonction.

Une fonction de nettoyage est utilisée pour libérer une ressource uniquement si un thread appelle pthread_exit ou est terminé par un autre thread à l'aide de pthread_cancel. Dans des circonstances normales, lorsqu'un thread ne se termine pas de manière forcée, la ressource doit être explicitement libérée et la fonction de nettoyage ne doit pas être appelée.

Pour enregistrer une telle fonction de nettoyage, utilisez:

void pthread_cleanup_push(void (*routine) (void *), void *arg);

Cette fonction reçoit les paramètres sous forme de pointeur sur la fonction en cours d'enregistrement et sur la valeur de l'argument à lui transmettre. La fonction 'routine' sera appelée avec l'argument 'arg' lorsque le thread sera forcé de sortir. Si plusieurs fonctions de nettoyage sont enregistrées, elles seront appelées dans LIFO (les dernières installées seront appelées en premier).

Pour chaque appel pthread_cleanup_push, il doit également exister l'appel approprié pthread_cleanup_pop]. ] qui a une fonction de nettoyage: <code c> void pthread_cleanup_pop(int execute); </code> Cela deînregistra plus récemment installé la fonction de nettoyage, et si le paramètre exécuter est nul et exécuter un testament. ** Attention ** appel A [[http://linux.die.net/man/3/pthread_cleanup_push|pthread_cleanup_push doit avoir un appel correspondant pthread_cleanup_pop dans funcţie aceeaşi et acelaşi niveau imbricare.

Un petit exemple d'utilisation des fonctions de nettoyage:

th_cleanup.c
void *alocare_buffer(int size)
{
	return malloc(size);
}
 
void dealocare_buffer(void *buffer)
{
	free(buffer);
}
 
/* la fonction appelée par un thread */
 
void functie()
{
	void *buffer = alocare_buffer(512);
 
	/ * Enregistrement de la fonction de nettoyage * /	pthread_cleanup_push(dealocare_buffer, buffer);
 
	/ * Ici, le traitement a lieu et pthread_exit peut être appelé
         ou le fil peut être terminé par un autre fil * /
	/ * pour enregistrer la fonction de nettoyage et son exécution
         (le paramètre donné est différent de zéro) * /	
        pthread_cleanup_pop(1);
}

Attributs d'un fil

Les attributs sont un moyen de spécifier un comportement différent du comportement par défaut. Quand un thread est créé avec pthread_create peut spécifier les attributs de ce fil. Les attributs par défaut sont suffisantes pour la plupart des applications. Avec un attribut peut changer:

  • Etat: unifié ou amovible

* la politique d'attribution de processeur pour le thread respectif (round robin, FIFO ou par défaut du système)

  • priorité (les personnes ayant la priorité la plus élevée seront planifiées, en moyenne, plus souvent)
  • taille et adresse de départ de la pile

Vous pouvez trouver plus de détails dans section dédiée.

Échec du processeur

Un thread exécute le droit d'exécution sur un autre thread après l'un des événements suivants:

  • Effectue un appel bloquant (demande d'E / S, synchronisation avec un autre thread) et le noyau décide qu'il profitable effectue un changement de contexte
  • Il a expiré le temps alloué par le noyau
  • cède volontairement le droit, en utilisant la fonction sched_yield]: :
    int sched_yield(void);

Si d'autres processeurs sont intéressés par le processeur, un processus capturera le processeur et s'il n'y a aucun processus en attente du processeur, le thread en cours continue l'exécution.

Autres opérations

Si nous voulons nous assurer qu'un code d'initialisation est exécuté une fois, nous pouvons utiliser la fonction:

pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));

Le but de pthread_once est de s'assurer qu'un morceau de code (habituellement utilisé pour l'initialisation) est exécuté une fois. L'argument 'once_control' est un pointeur sur une variable initialisée avec PTHREAD_ONCE_INIT . La première fois que cette fonction est appelée, elle appelle la fonction 'init_routine' et modifie la valeur de la variable 'once_control' pour se rappeler que l'initialisation a eu lieu. Les appels suivants de cette fonction avec le même once_control ne feront rien.

La fonction pthread_once renvoie 0 en cas d'échec ou le code de défaut en cas d'échec.

Pour déterminer si deux identificateurs font référence au même fil, vous pouvez utiliser:

int pthread_equal(pthread_t thread1, pthread_t thread2);

Les appels suivants sont disponibles pour apprendre / changer les priorités:

int pthread_setschedparam(pthread_t target_thread, int policy, const struct sched_param *param);
int pthread_getschedparam(pthread_t target_thread, int *policy, struct sched_param *param);

Compilation

La bibliothèque libpthread doit également être spécifiée (utilisez donc l'argument -lpthread ).

Ne liez pas un programme à un seul thread à cette bibliothèque. Certains appels provenant de bibliothèques standard peuvent avoir des implémentations de débogage plus inefficaces ou plus difficiles lors de l'utilisation de cette bibliothèque.

Exemple

Voici un exemple simple dans lequel deux threads sont créés, chacun affichant un caractère un certain nombre de fois à l'écran.

exemplu.c
#include <pthread.h>
#include <stdio.h>
 
/* parameter structure for every thread */
struct parameter {
	char character; /* printed character */
	int number;     /* how many times */
};
 
/* the function performed by every thread */
void* print_character(void *params)
{
	struct parameter *p = (struct parameter *) params;
	int i;
 
	for (i = 0; i < p->number; i++)
		printf("%c", p->character);
	printf("\n");
 
	return NULL;
}
 
int main()
{
	pthread_t fir1, fir2;
	struct parameter fir1_args, fir2_args;
 
	/* create one thread that will print 'x' 11 times */
	fir1_args.character = 'x';
	fir1_args.number = 11;
	if (pthread_create(&fir1, NULL, &print_character, &fir1_args)) {
		perror("pthread_create");
		exit(1);
	}
 
	/* create one thread that will print 'y' 13 times */
	fir2_args.character = 'y';
	fir2_args.number = 13;
	if (pthread_create(&fir2, NULL, &print_character, &fir2_args)) {
		perror("pthread_create");
		exit(1);
	}
 
	/* wait for completion */
	if (pthread_join(fir1, NULL))
		perror("pthread_join");
	if (pthread_join(fir2, NULL))
		perror("pthread_join");
 
	return 0;
}

La commande utilisée pour compiler cet exemple sera:

gcc -o exemplu exemplu.c -lpthread

Synchronisation des discussions

Pour synchroniser les threads, nous avons:

Mutex

Les mutations (verrous mutuellement exclus) sont des objets de synchronisation utilisés pour garantir l'accès dans une section de code qui utilise les données partagées entre deux ou plusieurs threads. Un mutex a deux états possibles: Occupé et Gratuit . Un mutex peut être occupé par un seul thread d'exécution à la fois. Lorsqu'un mutex est occupé par un thread, il ne peut plus être occupé par aucun autre thread. Dans ce cas, une demande d'occupation provenant d'un autre thread va généralement bloquer le thread jusqu'à ce que le mutex devienne libre.

Initialiser / Détruire un Mutex

Un mutex peut être initialisé / détruit de plusieurs manières:

  • en utilisant une macrodéfinition
    // Initialisation statique d'un mutex avec des attributs implicites
    // NB: le mutex n'est pas publié, la durée de vie du mutex
    // est la durée de vie du programme.
    pthread_mutex_t mutex_static = PTHREAD_MUTEX_INITIALIZER;
  • initialisation avec attributs par défaut (pthread_mutex_init, pthread_mutex_destroy)
    // signatures des fonctions d'initialisation et de destruction de mutex:
    int pthread_mutex_init   (pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
     
    void initializare_mutex_cu_atribute_implicite() {
        pthread_mutex_t mutex_implicit;
        pthread_mutex_init(&mutex_implicit, NULL); // atrr = NULL -> attributs par défaut
     
        // ... en utilisant mutex ...
     
        //libération de mutex
            pthread_mutex_destroy(&mutex_implicit);
    }
  • Initialisation avec attributs explicites

Détails pour l'initialisation avec des attributs explicites

Détails pour l'initialisation avec des attributs explicites

// NB: Fonction pthread_mutexattr_settype et macro PTHREAD_MUTEX_RECURSIVE
// sont uniquement disponibles si _XOPEN_SOURCE est défini à une valeur> = 500
// ** AVANT ** pour inclure <pthread.h>.
// Pour plus de détails, consultez feature_test_macros (7).
 
#defines _XOPEN_SOURCE 500
#include <pthread.h>
 
void initializare_mutex_recursiv() {
    // définit les attributs, les initialise et marque le type comme récursif.
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
 
    // définit un mutex récursif, initialisez-le avec les attributs définis précédemment
    pthread_mutex_t mutex_recursiv;
    pthread_mutex_init(&mutex_recursiv, &attr);
 
    // libère les attributs d'attribut après la création d'un mutex
    pthread_mutexattr_destroy(&attr);
 
    // ... en utilisant mutex ...
 
    //libération de mutex
    pthread_mutex_destroy(&mutex_recursiv);
}

Le mutex doit être gratuit pour être détruit . Sinon, la fonction retournera le code d'erreur EBUSY . Le retour de 0 signifie le succès de l'appel.

Types de mutex

A l'aide des attributs d'initialisation, vous pouvez créer des mutex avec des propriétés spéciales:

  • activation http://en.wikipedia.org/wiki/Priority_inheritance priorité prioritaire ( héritage priorité ) pour empêcher inversion de priorité ( inversion de priorité ). Il existe trois protocoles d'héritage prioritaire:
  • PTHREAD_PRIO_NONE - n'hérite pas de la priorité lorsque nous possédons le mutex créé avec cet attribut
    • PTHREAD_PRIO_INHERIT - si un mutex est créé avec cet attribut et si des threads d'exécution sont bloqués sur ce mutex, héritez de la priorité du thread avec priorité la plus élevée
    • PTHREAD_PRIO_PROTECT - Si le thread actuel a un ou plusieurs mutex, il s'exécutera à maximum de spécifié pour tous les mutex possédés.

Click to display ⇲

Click to hide ⇱

#define _XOPEN_SOURCE 500
#include <pthread.h>
 
int pthread_mutexattr_getprotocol(const pthread_mutexattr_t *attr, int *protocol);
int pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, int protocol);
  • mode de comportement à prise de contrôle récursive du mutex
    • PTHREAD_MUTEX_NORMAL - aucun contrôle n'est effectué, une prise récursive mène à impasse
    • PTHREAD_MUTEX_ERRORCHECK - les vérifications sont effectuées, les prises récursives conduisent à l'erreur
    • PTHREAD_MUTEX_RECURSIVE - les mutex peuvent être pris de manière récursive mais doivent être libérés le même nombre de fois .
#define _XOPEN_SOURCE 500
#include <pthread.h>
 
pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *protocol);
pthread_mutexattr_settype(pthread_mutexattr_t *attr, int protocol);

Occupation / Libération d'un mutex

Fonctions de blocage / libération de mutex (pthread_mutex_lock], pthread_mutex_unlock):

int pthread_mutex_lock (pthread_mutex_t * mutex);
int pthread_mutex_unlock (pthread_mutex_t * mutex);

Si le mutex est libre au moment de l'appel, il sera occupé par la ligne appelante et la fonction reviendra immédiatement. Si le mutex est occupé par un autre , l'appel sera verrouillé jusqu'à ce que le mutex soit libéré. Si le mutex est déjà occupé par le thread d'exécution en cours (verrou récursif), le comportement de la fonction est dicté par le type de mutex:

Type de mutex Verrouillage récursif Déverrouiller
PTHREAD_MUTEX_NORMAL impasse libère mutex
PTHREAD_MUTEX_ERRORCHECK erreur de retour libère mutex
PTHREAD_MUTEX_RECURSIVE incrémente le nombre d'occupants décrémente le nombre d'occupants (zéro libère le mutex)
PTHREAD_MUTEX_DEFAULT impasse libère mutex

Il n'y a pas d'ordre FIFO pour occuper un mutex. L’une des broches en attente d’un mutex à déverrouiller peut le capturer.

Le test non bloquant d’occupation d’un mutex

Pour essayer d'occuper un mutex sans attendre pour le libérer s'il est déjà occupé, appelez pthread_mutex_trylock:

int pthread_mutex_trylock(pthread_mutex_t *mutex);

Exemple:

int rc = pthread_mutex_trylock(&mutex);
if (rc == 0) {
    /* successfully aquired the free mutex */
} else if (rc == EBUSY) {
    /* mutex was held by someone else
       instead of blocking we return EBUSY */
} else {
    /* some other error occured */
}

Exemple d'utilisation de mutex

Exemple d'utilisation d'un mutex pour sérialiser l'accès à la variable globale global_counter:

#include <stdio.h>
#include <pthread.h>
 
#define NUM_THREADS 5
 
/* global mutex */
pthread_mutex_t mutex;
int global_counter = 0;
 
void *thread_routine(void *arg) 
{    
    /* acquire global mutex */
    pthread_mutex_lock(&mutex);
 
    /* print and modify global_counter */
    printf("Thread %d says global_counter=%d\n", (int) arg, global_counter);
    global_counter++;
 
    /* release mutex - now other threads can modify global_counter */
    pthread_mutex_unlock(&mutex);
 
    return NULL;
}
 
int main(void) 
{
    int i;
    pthread_t tids[NUM_THREADS];
 
    /* init mutex once, but use it in every thread */
    pthread_mutex_init(&mutex, NULL);
 
    /* all threads execute thread_routine
       as args to the thread send a thread id 
       represented by a pointer to an integer */
    for (i = 0; i < NUM_THREADS; i++)
        pthread_create(&tids[i], NULL, thread_routine, (void *) i);
 
    /* wait for all threads to finish */
    for (i = 0; i < NUM_THREADS; i++)
        pthread_join(tids[i], NULL);
 
    /* dispose mutex */
    pthread_mutex_destroy(&mutex);
 
    return 0;
}
so@spook$ gcc -Wall mutex.c -lpthread
so@spook$ ./a.out 
Thread 1 says global_counter=0
Thread 2 says global_counter=1
Thread 3 says global_counter=2
Thread 4 says global_counter=3
Thread 0 says global_counter=4

Futex

Les mutex des exécutions POSIX sont implémentés avec futex pour des raisons de performances.

L’optimisation consiste à tester et à paramétrer de manière atomique la valeur du mutex (par une instruction test-and-set-lock ) dans l’espace utilisateur, éliminant ainsi l’interruption du noyau en cas de dans lequel aucun blocage n'est requis.

Détails sur les futex

Détails sur les futex

Le futex nom vient de Fast User-space muTEX . L'idée derrière la mise en œuvre de futex était d'optimiser l'occupation mutex si elle n'est pas déjà occupée . Si le mutex n'est pas occupé, il le sera sans que le processus ne le bloque. Dans ce cas, et il n'est pas nécessaire de bloquer, il n'est pas nécessaire que le processus entre en mode noyau (pour entrer dans une file d'attente). L’optimisation consiste à tester et à paramétrer de manière atomique la valeur du mutex (par une instruction test-and-set-lock ) dans l’espace utilisateur, éliminant ainsi l’interruption du noyau en cas de dans lequel aucun blocage n'est requis.

Futex peut être n'importe quelle variable dans une zone de mémoire partagée entre plusieurs threads ou processus. Ainsi, les opérations réelles avec futex sont effectuées par la fonctionnalité do_futex , disponible en incluant l'en-tête linux / futex.h . Sa signature ressemble à ceci:

long do_futex(unsigned long uaddr, int op,
              int val, unsigned long timeout, unsigned long uaddr2, int val2);

Si le blocage est requis, do_futex fera un appel système - sys_futex . Futex peut être utile (et doit parfois être explicitement utilisé) en cas de synchronisation de processus, étant alloué en variables à partir de zones de mémoire partagée entre ces processus.

Semafor

Les sémaphores sont des objets de synchronisation qui représentent une généralisation de mutex dans la mesure où enregistre le nombre d'opérations de libération (incrémentielles) effectuées sur eux. En fait, un feu de signalisation est un atome qui augmente / diminue. La valeur d'un feu de signalisation ne peut pas tomber en dessous de 0. Si le feu de signalisation est à 0, l'opération de décrémentation sera verrouillée jusqu'à ce que le feu devienne strictement positif. Les mutex peuvent donc être considérés comme des sémaphores binaires.

Les drapeaux POSIX sont de deux types:

  • 'avec noms' '- généralement utilisé pour la synchronisation entre processus distincts;
  • nonsense - qui peut être utilisé pour synchroniser les threads du même processus ou entre processus - à condition que le sémaphore se trouve dans une zone de mémoire partagée.

Les différences entre les espaces de noms de premier nom et non nommés apparaissent dans les fonctions de création et de destruction, les autres fonctions étant identiques.

  • Les deux types de feux de signalisation sont représentés dans le code par le type sem_t .
  • Les étiquettes de nom sont identifiées au niveau du système via une chaîne de caractères '/ nom' .
  • les fichiers d'en-tête requis sont <fcntl.h> , <sys / types.h> et <semaphore.h> .

Les opérations pouvant être effectuées sur les sémaphores POSIX sont multiples:

Mémoires de noms - Initialisation / Désinitialisation

/* use named semaphore to synchronize processes */
/* open */
sem_t* sem_open(const char *name, int oflag);                                 
/* create if oflag has O_CREAT set */
sem_t* sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
 
/* close named semaphore */
int sem_close(sem_t *sem);
 
/* delete a named semaphore from system */
int sem_unlink(const char *name);
 

Détails des fonctions sem_open

Détails des fonctions sem_open

Le comportement est similaire à l'ouverture de fichier. Si l'indicateur O_CREAT est présent, la deuxième forme de la fonction doit être utilisée, en spécifiant les autorisations et la valeur initiale.

Les seules possibilités pour le deuxième argument sont les suivantes:

  • 0 - s'allume s'il y a
  • O_CREAT - le sémaphore est créé s'il n'existe pas; il s'ouvre s'il existe
  • O_CREAT | O_EXCL - le sémaphore est créé seulement s'il n'existe pas; une erreur est renvoyée si elle existe </spoiler> ==== Sémantique anonyme - Initialisation / Désinitialisation ==== /
  • initialisation d'un feu de signalisation sans nom
  • sem - le sémaphore nouvellement créé
  • pshared - 0 si le sémaphore est partagé uniquement

par les fils du processus actuel

  1. non nul: feu de signalisation partagé avec d'autres processus

dans ce cas, 'ici' doit être attribué dans une zone

           mémoire partagée
* valeur - la valeur initiale du feu
* /
int sem_init(sem_t *sem, int pshared, unsigned int value);
 
/* close unnamed semaphore */
int sem_destroy(sem_t *sem);

Opérations courantes sur les feux de signalisation

/* increment/release semaphore (V) */
int sem_post(sem_t *sem);
 
/* decrement/acquire semaphore (P) */
int sem_wait(sem_t *sem);
 
/* non-blocking decrement/acquire */
int sem_trywait(sem_t *sem);
 
/* getting the semaphore count */
int sem_getvalue(sem_t *sem, int *pvalue);

Exemple d'utilisation d'un sémaphore avec le nom

#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <semaphore.h>
 
#include "utils.h"
 
#define	SEM_NAME	"/my_semaphore"
 
int main(void)
{
	sem_t *my_sem;
	int rc, pvalue;
 
	/* create semaphore with initial value of 1 */
	my_sem = sem_open(SEM_NAME, O_CREAT, 0644, 1); 
	DIE(my_sem == SEM_FAILED, "sem_open failed");
 
 
	/* get the semaphore */
	sem_wait(my_sem);
 
	/* do important stuff protected by the semaphore */
	rc = sem_getvalue(my_sem, &pvalue);
	DIE(rc == -1, "sem_getvalue");
	printf("sem is %d\n", pvalue);
 
	/* release the lock */
	sem_post(my_sem);
 
	rc = sem_close(my_sem);
	DIE(rc == -1, "sem_close");
 
	rc = sem_unlink(SEM_NAME);
	DIE(rc == -1, "sem_unlink");
 
	return 0;
}

Le sémaphore sera créé dans / dev / shm et portera le nom sem.my_semaphore .

Variables de condition

Les variables de condition fournissent un système de notification de thread d'exécution permettant à un thread de se bloquer en attendant le signal d'un autre thread. L'utilisation correcte des variables de condition implique un protocole coopératif entre les threads d'exécution.

Les mutex et les sémaphores permettent à d'autres threads de bloquer . Les variables de condition servent à bloquer le thread en cours jusqu'à ce qu'une condition soit remplie.

Les variables de condition sont des objets de synchronisation qui permettent à un thread de suspendre son exécution jusqu'à ce qu'une condition (logique du prédicat) devienne vraie . Lorsqu'un thread d'exécution détermine que le prédicat est devenu vrai, il signale la condition de variable, déverrouillant ainsi un ou tous les threads d'exécution bloqués à cette condition de variable (par intention).

Une condition variable doit toujours être utilisée avec un mutex pour éviter la course qui se produit lorsqu'un thread se prépare à attendre la condition de variable après avoir évalué le prédicat logique et qu'un autre thread indique la condition de variable même avant le premier thread. blocage, perdant ainsi le signal. Par conséquent, les opérations de signalisation, les tests de condition logique et la condition de verrouillage à la condition doivent être effectués avec le mutex occupé associé à la variable de condition. La condition logique est testée sous la protection du mutex, et si elle n'est pas remplie, le fil appelant se bloque lui-même à la condition de variable, libérant le mutex de manière atomique. Au déverrouillage, un thread d'exécution essaiera d'occuper le mutex associé à la condition de variable. En outre, le test des prédicats logiques doit être effectué dans une boucle , car si plusieurs fils sont libérés à la fois, un seul sera en mesure de gérer le mutex associé à la condition. Le reste attendra qu'il le libère, mais il se peut que le fil du mutex change la valeur du prédicat logique pendant que le mutex est en place. Pour cette raison, les autres fils doivent tester à nouveau le prédicat car sinon, son exécution commencerait en supposant que le prédicat vrai est en réalité faux.

Initialiser / détruire une variable de condition

L'initialisation d'une variable de condition est effectuée à l'aide de la macro PTHREAD_COND_INITIALIZER ou de la fonction pthread_cond_init. La destruction d'une variable de condition est effectuée par pthread_cond_destroy.

// initialisation statique d'une variable de condition avec des attributs implicites
// NB: la variable condition n'est pas libérée,
// la durée de vie de la variable condition est la durée de vie du programme.
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
 
// Signatures d'initialisation et de libération des variables de condition:
int pthread_cond_init   (pthread_cond_t *cond, pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);

Comme Mutex:

  • si le paramètre 'attr' est NULL, les attributs par défaut sont utilisés
  • il ne doit pas y avoir de fil en attente sur la variable de condition quand elle est détruite, sinon elle retourne EBUSY .

Verrouiller une condition de variable

Pour suspendre l'exécution et attendre une condition de variable, un fil appelle la fonction pthread_cond_wait:

int pthread_cond_wait (pthread_cond_t * cond, pthread_mutex_t * mutex);

Le thread appelant doit être déjà associé au mutex au moment de l'appel. La fonction 'pthread_cond_wait' va libérer le mutex et bloquer , en attendant que la condition de variable soit marquée par un autre thread. Les deux opérations sont atomiques . Lorsque la condition de variable est signalée, il essaiera d’occuper le mutex associé et, après occupant , l’appel de fonction reviendra. Notez que le thread appelant peut être suspendu après le déverrouillage en attendant que le mutex associé soit occupé, tandis que le prédicat logique, true lors du déverrouillage du thread, peut être modifié par d'autres threads. Par conséquent, l'appel pthread_cond_wait doit être effectué dans une boucle où la valeur prédictive de vérité associée à la variable de condition est testée pour garantir une sérialisation correcte des threads. Un autre argument en faveur du test de boucle du prédicat logique est qu'un appel pthread_cond_wait peut être interrompu par un signal asynchrone (voir laboratoire du signal) avant que le prédicat logique ne devienne vrai. Si les unités d'exécution en attente de la condition de variable ne testent pas à nouveau le prédicat logique, elles poursuivent leur exécution en supposant que cela est vrai.

Verrouiller une condition variable avec timeout

Afin de suspendre l'exécution et d'attendre une condition de variable, au plus tard à une heure spécifiée, un thread d'exécution appellera | pthread_cond_timedwait:

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, 
                           const struct timespec *abstime);

La fonction se comporte de la même façon que pthread_cond_wait , sauf que si la condition de variable n'est pas signalée avant 'abstime' ', la ligne appelante est déverrouillée et, après avoir occupé le mutex associé, la fonction retourne erreur ETIMEDOUT . Le paramètre 'abstime' est absolu et représente le nombre de secondes écoulées depuis le 1er janvier 1970 à 00:00.

Déverrouiller un seul thread verrouillé dans une condition de variable

Pour déverrouiller un seul thread de verrouillage sur une variable de condition, la variable de condition sera signalée à l'aide de pthread_cond_signal:

int pthread_cond_signal (pthread_cond_t * cond);

Si aucun thread n'attend la variable, l'appel de fonction ne fonctionne pas et la signalisation perd . Si plusieurs threads attendent la variable variable, un seul d'entre eux sera déverrouillé. Le choix du thread à déverrouiller est effectué par le concepteur de thread. On ne peut pas supposer que les threads en attente seront déverrouillés dans l'ordre dans lequel leur attente commence. Le thread appelant doit avoir le mutex associé à la variable de condition lors de l'appel de cette fonction.

Exemple:

pthread_mutex_t count_lock;
pthread_cond_t  count_nonzero;
unsigned        count;
 
void decrement_count() {
    pthread_mutex_lock(&count_lock);
    while (count == 0)
        pthread_cond_wait(&count_nonzero, &count_lock);
    count = count - 1;
    pthread_mutex_unlock(&count_lock);
}
 
void increment_count() {
    pthread_mutex_lock(&count_lock);
    count = count + 1;
    pthread_cond_signal(&count_nonzero);
    pthread_mutex_unlock(&count_lock);
}

Déverrouillez tous les threads verrouillés avec une variable de condition

Pour déverrouiller tous les threads d'exécution bloqués au niveau d'une variable de condition, la variable de condition est marquée à l'aide de pthread_cond_broadcast:

int pthread_cond_broadcast (pthread_cond_t * cond);

Si aucun thread n'attend la variable, l'appel de fonction ne fonctionne pas et la signalisation perd . Si la variable de condition attend des threads d'exécution, tout sera déverrouillé, mais sera en concurrence pour occuper le mutex associé à la variable condition. Le thread appelant doit avoir le mutex associé à la condition de variable lors de l'appel de cette fonctionnalité.

Exemple d'utilisation de variables de condition

Le programme suivant utilise une barre pour synchroniser les threads du programme. La barrière est implémentée à l'aide d'une variable de condition.

#include <stdio.h>
#include <pthread.h>
 
#define NUM_THREADS 5
 
// implémente une barrière * non réutilisable avec des variables de condition
struct my_barrier_t {
    // mutex utilisé pour sérialiser l'accès aux données de barrière internes
    pthread_mutex_t lock;
 
    // la variable de condition qui devrait arriver à tous les threads
    pthread_cond_t cond;
 
    // le nombre de threads qui doivent venir pour libérer la barrière
    int nr_still_to_come;
};
 
structure my_barrier_t bar;
 
void my_barrier_init (struct my_barrier_t * bar, int nr_still_to_come) {
    pthread_mutex_init (& bar-> lock, NULL);
    pthread_cond_init (& bar-> cond, NULL);
 
    // combien de threads sont attendus à la barrière
    bar-> nr_still_to_come = nr_still_to_come;
}
 
void my_barrier_destroy (struct my_barrier_t * bar) {
    pthread_cond_destroy (& bar-> cond);
    pthread_mutex_destroy (& bar-> serrure);
}
 
void * thread_routine (void * arg) {
    int thd_id = (int) arg;
 
    // Avant de travailler avec les données de la barrière interne, il faut prendre le mutex
    pthread_mutex_lock (& bar.lock);
 
    printf ("thd% d: avant la barrière \ n", thd_id);
 
    // Sommes-nous le dernier thread d'exécution qui est arrivé à la barrière?
    int is_last_to_arrive = (bar.nr_still_to_come == 1);
    // décrémente le nombre de threads attendus à la barrière
    bar.nr_still_to_come --;
 
    // Alors qu'il y a d'autres threads qui n'ont pas atteint la barrière, nous attendons.
    while (bar.nr_still_to_come! = 0)
        // le mutex est libéré automatiquement avant le début de l'attente
        pthread_cond_wait (& bar.cond, & bar.lock);
 
    // le dernier thread de la barrière signalera les autres threads
    if (is_last_to_arrive) {
        printf ("laissez l'inondation dans \ n");
        pthread_cond_broadcast (& bar.cond);
    }
 
    printf ("thd% d: après la barrière \ n", thd_id);
 
    // La sortie du mutex enlève automatiquement le mutex à quitter
    pthread_mutex_unlock (& bar.lock);
 
    return NULL;
}
 
int main (void)
    int i;
    pthread_t indique [NUM_THREADS];
 
    my_barrier_init (& bar, NUM_THREADS);
 
    for(i = 0; i <NUM_THREADS; i ++)
        pthread_create (& tids [i], NULL, thread_routine, (void *) i);
 
    for (i = 0; i <NUM_THREADS; i ++)
        pthread_join (informations [i], NULL);
 
    my_barrier_destroy (& bars);
 
    return 0;
}
so @ spook $ gcc -Wall cond_var.c -pthread
donc @ spook $ ./a.out
thd 0: avant la barrière
2e 2: devant la barrière
thd 3: devant la barrière
thd 4: devant la barrière
1 er: devant la barrière
    laisser l'inondation dans
1 er: après la barrière
2e 2: après la barrière
3 ème: après la barrière
thd 4: après la barrière
thd 0: après la barrière

La mise en œuvre du programme montre:

  • l'ordre dans lequel les threads sont planifiés n'est pas nécessairement celui de leur création
  • l'ordre dans lequel les exécutions en attente d'une variable de condition sont réveillées n'est pas nécessairement l'ordre dans lequel elles étaient en attente.

Barrière

La norme POSIX définit également un ensemble de fonctions et de structures de données de barrière. Ces fonctions sont disponibles si la macro _XOPEN_SOURCE est définie à une valeur> = 600.

Initialiser / Détruire une barrière

La barrière sera initialisée à l'aide de pthread_barrier_init et sera détruite à l'aide de pthread_barrier_destroy .

// pour utiliser les fonctions de barrière doit être défini
// _XOPEN_SOURCE à une valeur> = 600. Pour plus de détails, voir feature_test_macros (7).
#define _XOPEN_SOURCE 600
#include <pthread.h>
 
// attr -> un ensemble d'attributs, peut être NULL (les attributs par défaut sont utilisés)
// count -> le nombre de threads qui doivent arriver
// à la barrière pour le libérer
int pthread_barrier_init (pthread_barrier_t * barrière,
                         const pthread_barrierattr_t * attr,
                         unsigned count);
 
// il ne doit y avoir aucun thread en attente à la barrière
// avant d'appeler la fonction _destroy, sinon EBUSY retourne
// ne détruis pas la barrière.
int pthread_barrier_destroy (pthread_barrier_t * barrier);

En attente d'une barrière

L’attente de la barrière est terminée en appelant pthread_barrier_wait:

#define _XOPEN_SOURCE 600
#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);

Si la barrière a été créée avec compte = N , les premiers threads N-1 qui appellent pthread_barrier_wait sont bloqués. Lorsque le dernier (N) apparaît, il déverrouille tous les threads N-1. La fonction pthread_barrier_wait renvoie trois valeurs:

  • EINVAL - si la barrière n'est pas initialisée (seul défaut défini)
  • PTHREAD_BARRIER_SERIAL_THREAD - en cas de succès, un seul thread renverra cette valeur - il n'est pas spécifié quel thread est exécuté (il n'est pas nécessaire que le dernier ait atteint la barrière)
  • 0 - valeur renvoyée en cas de succès par les autres threads N-1.

Exemple d'utilisation de la barrière

Avec les barrières POSIX, le programme ci-dessus peut être simplifié:

#define _XOPEN_SOURCE 600
#include <pthread.h>
#include <stdio.h>
 
#define NUM_THREADS 5
 
pthread_barrier_t barrier;
 
void *thread_routine(void *arg) {
    int thd_id = (int) arg;
    int rc;
 
    printf("thd %d: before the barrier\n", thd_id);
 
    // tous les threads en attente à la barrière.
    rc = pthread_barrier_wait(&barrier);
    if (rc == PTHREAD_BARRIER_SERIAL_THREAD) {
        // un seul thread (éventuellement le dernier) retournera PTHREAD_BARRIER_SERIAL_THREAD
        // le reste des discussions renvoie 0 en cas de succès.
        printf("   let the flood in\n"); 
    }
 
    printf("thd %d: after the barrier\n", thd_id);
 
    return NULL;
}
 
int main (void)
{
    int i;
    pthread_t tids[NUM_THREADS];
 
    // La barrière est initialisée une fois et utilisée par tous les threads
    pthread_barrier_init(&barrier, NULL, NUM_THREADS);
 
    // les exécutions exécuteront le code de fonction thread_routine.
    // au lieu d'un pointeur sur des données utiles, il est envoyé dans le dernier argument
    // un entier - l'identifiant du thread
   for (i = 0; i < NUM_THREADS; i++)
        pthread_create(&tids[i], NULL, thread_routine, (void *) i);
 
    // nous nous attendons à ce que tous les threads se terminent
    for (i = 0; i < NUM_THREADS; i++)
        pthread_join(tids[i], NULL);
 
    // Nous libérons les ressources de la barrière
    pthread_barrier_destroy(&barrier);
 
    return 0;
}
so@spook$ gcc -Wall barrier.c -lpthread
so@spook$ ./a.out 
thd 0: before the barrier
thd 2: before the barrier
thd 1: before the barrier
thd 3: before the barrier
thd 4: before the barrier
   let the flood in
thd 4: after the barrier
thd 2: after the barrier
thd 3: after the barrier
thd 0: after the barrier
thd 1: after the barrier

Exercices de laboratoire

Exercice 0 - Jeu interactif (2p)

  • Détails jeu..

Linux (9p)

Pour résoudre le laboratoire, utilisez l'archive de tâches lab08-tasks.zip

Pour vous aider à mettre en œuvre les exercices de laboratoire, il existe un fichier utils.h avec des fonctions utiles dans le répertoire utils de l'archive.

Pour installer les pages de manuel de 'pthreads'

 sudo apt-get install manpages-posix manpages-posix-dev 

Exercice 1 - Pile de fils (2p)

Allez dans le répertoire 1-th_stack et inspectez la source, puis compilez et exécutez le programme. Suivez pmap ou 'procfs' 'pour modifier l'espace d'adressage du programme:

watch -d pmap $(pidof th_stack)
watch -d cat /proc/$(pidof th_stack)/maps

Les zones de mémoire de 8 Mo (8192 Ko) créées après chaque appel pthread_create représentent les nouvelles piles allouées par la bibliothèque libpthread pour chaque thread. Notez qu'une page (4Ko) avec la protection ' --- p ' (PROT_NONE, private - visible dans procfs ) joue le rôle de " garde de page " .

La raison pour laquelle le programme ne se termine pas est la présence d'une fonction “while (1)” dans la fonction thread. Utilisez Ctrl + C pour terminer le programme.

Exercice 2 - Processus d'exécution vs processus (2p)

Accédez au répertoire '2-th_vs_proc' et inspectez les sources. Les deux programmes simulent un serveur qui crée des threads / processus. Compilez et exécutez les deux programmes un à la fois.

Lors de l'exécution, vous affichez dans une autre console le nombre de threads / processus créés dans les deux situations à l'aide de la commande ps-L-c <nom_programme> .

ps -L -C threads
ps -L -C processes

Vérifiez ce qui se passe si, à un moment donné, un thread en cours de fonctionnement meurt (ou un processus, en fonction de l'exécutable que vous testez). Testez en utilisant la fonction 'do_bad_task' sur chaque 4ème thread / processus.

Exercice 3 - Sécurité du fil (2p)

Etant donné que la machine virtuelle spook n'a qu'un seul noyau virtuel, le prochain exercice doit être effectué sur la machine physique pour permettre à plusieurs threads de s'exécuter simultanément.

Allez dans le répertoire 3-safety et inspectez la source malloc.c . Les fonctions thread_function et main NOT sont thread-safe par rapport aux variables global_storage et function_global_storage(voir la signification de thread safety ). Il existe un Race condition entre les deux threads créés en incrémentant la variable function_global_storage déclarée dans la fonction thread_function et une autre condition d'exécution entre tous les threads de processus lors de l'incrémentation de la variable globale global_storage.

“Helgrind” est un utilitaire très utile, capable de détecter automatiquement ces conditions de concurrence. Nous pouvons l'utiliser comme suit:

valgrind --tool=helgrind ./mutex

* nous allons nous concentrer sur la résolution de TODO1 sur un mutex exécutable TODO 1 : pour résoudre ces deux conditions de concurrence, appelez la fonction de plus-value en mode thread_safe avec l'aide de l'API fournie par critical.h

Le fichier malloc.c crée NUM_THREADS qui alloue de la mémoire en 1000 tours. Il existe des chances que les threads exécutent des appels “malloc” concurrents. Une fois que vous avez résolu le problème, compilez et exécutez plusieurs fois. Nous remarquons que le programme fonctionne avec succès. Pour faire des vérifications supplémentaires, nous lançons à nouveau helgrind :

valgrind --tool=helgrind ./mutex

Nous notons que ni 'helgrind' ne rapporte aucune erreur, ce qui a pour conséquence que la fonction malloc est thread-safe. (même s'il n'est pas protégé par l'API fournie) Pour être sûr, nous devons examiner les pages de manuel et le code source.

Il est important de savoir que certaines fonctions sont thread-safe et que d'autres ne le sont pas. Recherchez une liste des fonctionnalités non liées aux threads dans la page de manuel pthreads(7) dans le ' Fonctions de thread-safe '.

La fonction 'malloc' de l'implémentation GLIBC est thread-safe , comme indiqué dans la page de manuel malloc (3) (troisième paragraphe de la section “NOTES”) et visible dans le code source par la présence du champ “mutex” dans structure malloc_state.

TODO 2 : Implémente un spinlock en utilisant des opérations atomiques. Les opérations atomiques existantes dans la norme GCC peuvent être consultées à l’adresse fonctions __atomiques

Dans le fichier critical.c, vous devez renseigner les commentaires TODO 2 associés.

Testez et exécutez plusieurs fois pour vérifier la cohérence de la variable globale: global_storage, l'exécutable ./spin .

Exercice 4 - Bloqué (2p)

Le fichier répertoire Inspectez de blocked.c 4-bloqué , compiler et exécuter binaire (répéter jusqu'à un logiciel de blocage détecté). Le programme crée deux threads à la recherche d'un nombre magique, chacun dans son propre (pas nécessairement le nombre à trouver). Chaque fil pour chaque valeur de sa gamme, vérifiez si la demande:

  • Si oui, indiquez le champ trouvé de laisser l'autre fil que l'on trouve le nombre recherché.
  • Dans le cas contraire, inspecter le champ trouvé l'autre structure de fil pour voir si elle a déjà trouvé le numéro recherché.

Déterminer le blocage de la cause, le programme de réparation et d'expliquer la solution. Vous pouvez utiliser Helgrind , l'un des sites d'outils de Valgrind pour détecter le problème: <bash code> $ valgrind –tool=helgrind ./blocked </code>

Comme le montre et Helgrind Le problème est que les deux fils prennent deux années mutex dans l'ordre inverse, le très susceptible de causer un deadlock.

Exercice 5 - Mise en œuvre comportement pthread_once (1p)

Vous fonctionnez l'initialisation que vous voulez appeler une fois. À partir de la source once.c répertoire 5-once , assurez-vous que la fonction init_func est appelée une fois. Vous ne pouvez pas modifier la fonction init_func ou utiliser pthread_once .

Lisez à propos de la fonctionnalité pthread_once et consultez la section sur mutex.

Exercice 6 - Mutex contre Spinlock (1p)

Nous voulons tester quelle variante est la plus efficace pour protéger l’incrément d’une variable.

Allez dans le répertoire 6-spin , inspectez et compilez la source spin.c . La compilation donnera deux exécutables, l’un utilisant un mutex pour la synchronisation et l’autre un spinlock.

Comparer les temps d'exécution:

time ./mutex
time ./spin

Lorsqu'un thread trouve le mutex occupé, il se verrouille. Lorsqu'un thread trouve le spinlock occupé, il sera occupé.

Ressources utiles

sde/laboratoare/08.1554876585.txt.gz · Last modified: 2019/04/10 09:09 (external edit)
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