This is an old revision of the document!
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).
/ dev / sda1
). (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).
É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:
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).
Du point de vue de la mise en œuvre, il existe 3 catégories de threads:
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.
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)
None
Thread-N
, ou N est un numérotarget
;
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()
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.
join
dans le programme principal pour les threads qu'elle génere.
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.
join
sur un thread de type daemon.
Un thread finit son exécution automatiquement, a la fin du code du fil 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
, 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
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()
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:
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.
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);
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.
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:
* la politique d'attribution de processeur pour le thread respectif (round robin, FIFO ou par défaut du système)
Vous pouvez trouver plus de détails dans section dédiée.
Un thread exécute le droit d'exécution sur un autre thread après l'un des événements suivants:
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.
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);
La bibliothèque libpthread doit également être spécifiée (utilisez donc l'argument -lpthread
).
Voici un exemple simple dans lequel deux threads sont créés, chacun affichant un caractère un certain nombre de fois à l'écran.
#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
Pour synchroniser les threads, nous avons:
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.
Un mutex peut être initialisé / détruit de plusieurs manières:
// 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;
// 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); }
EBUSY
. Le retour de 0
signifie le succès de l'appel.
A l'aide des attributs d'initialisation, vous pouvez créer des mutex avec des propriétés spéciales:
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.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.
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 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
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.
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:
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.
sem_t
. '/ nom'
. <fcntl.h>
, <sys / types.h>
et <semaphore.h>
.Les opérations pouvant être effectuées sur les sémaphores POSIX sont multiples:
/* 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);
/* 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);
#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
.
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.
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:
EBUSY
.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.
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.
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); }
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é.
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:
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.
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);
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.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
Pour résoudre le labo, veuillez cloner repository. si vous en avez déjà un, lancez svp git pull
.
utils.h
avec des fonctions utiles dans le répertoire utils
de l'archive.
sudo apt-get install manpages-posix manpages-posix-dev
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.
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.
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
.
valgrind --tool=helgrind ./mutex
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.
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
.
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:
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>
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.
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