Programmation multitâches LinuxThreads. Labo-Unix - http ://www.labo-unix.net

Documents pareils
03/04/2007. Tâche 1 Tâche 2 Tâche 3. Système Unix. Time sharing

Les processus légers : threads. Système L3, /31

Introduction à la programmation concurrente

1. Structure d un programme C. 2. Commentaire: /*..texte */ On utilise aussi le commentaire du C++ qui est valable pour C: 3.

INTRODUCTION À LA PROGRAMMATION CONCURRENTE

INTRODUCTION AUX SYSTEMES D EXPLOITATION. TD2 Exclusion mutuelle / Sémaphores

Cours d Algorithmique-Programmation 2 e partie (IAP2): programmation 24 octobre 2007impérative 1 / 44 et. structures de données simples

Exécutif temps réel Pierre-Yves Duval (cppm)

Le langage C. Séance n 4

1 Mesure de la performance d un système temps réel : la gigue

Cours de Systèmes d Exploitation

Programmation système en C/C++

Programmation Système (en C sous linux) Rémy Malgouyres LIMOS UMR 6158, IUT département info Université Clermont 1, B.P.

1/24. I passer d un problème exprimé en français à la réalisation d un. I expressions arithmétiques. I structures de contrôle (tests, boucles)

Problèmes liés à la concurrence

Le prototype de la fonction main()

Programmation impérative

Cours Programmation Système

INITIATION AU LANGAGE C SUR PIC DE MICROSHIP

DAns un système d exploitation multiprogrammé en temps partagé, plusieurs

Exclusion Mutuelle. Arnaud Labourel Courriel : arnaud.labourel@lif.univ-mrs.fr. Université de Provence. 9 février 2011

IN Cours 1. 1 Informatique, calculateurs. 2 Un premier programme en C

Premiers Pas en Programmation Objet : les Classes et les Objets

3IS - Système d'exploitation linux - Programmation système

Introduction au langage C

Programmation système I Les entrées/sorties

École Polytechnique de Montréal. Département de Génie Informatique et Génie Logiciel. Cours INF2610. Contrôle périodique.

Utilisation d objets : String et ArrayList

Éléments d informatique Cours 3 La programmation structurée en langage C L instruction de contrôle if

Algorithmique et Programmation, IMA

Conventions d écriture et outils de mise au point

REALISATION d'un. ORDONNANCEUR à ECHEANCES

TD3: tableaux avancées, première classe et chaînes

Exceptions. 1 Entrées/sorties. Objectif. Manipuler les exceptions ;

OS Réseaux et Programmation Système - C5

Cours d initiation à la programmation en C++ Johann Cuenin

Threads. Threads. USTL routier 1

Java Licence Professionnelle CISII,

Cours 1: Java et les objets

Programmation C++ (débutant)/instructions for, while et do...while

ENSP Strasbourg (Edition ) Les Systèmes Temps Réels - Ch. DOIGNON. Chapitre 3. Mise en œuvre : signaux, gestion du temps et multi-activités

Programmation système

Programmation en langage C

Performances de la programmation multi-thread

Synchro et Threads Java TM

Bases de programmation. Cours 5. Structurer les données

Centre CPGE TSI - Safi 2010/2011. Algorithmique et programmation :

TD Objets distribués n 3 : Windows XP et Visual Studio.NET. Introduction à.net Remoting

Outils pour la pratique

Programmation système de commandes en C

Introduction à la programmation orientée objet, illustrée par le langage C++ Patrick Cégielski

Brefs rappels sur la pile et le tas (Stack. / Heap) et les pointeurs

Systèmes temps-réel. Plan général. Matthieu Herrb. Mars Introduction - concepts généraux

Exercices INF5171 : série #3 (Automne 2012)

TP Temps Réel. Polytech Paris - Mars 2012

Structure d un programme et Compilation Notions de classe et d objet Syntaxe

DE L ALGORITHME AU PROGRAMME INTRO AU LANGAGE C 51

Les débordements de tampons et les vulnérabilités de chaîne de format 1

Le Langage C Version 1.2 c 2002 Florence HENRY Observatoire de Paris Université de Versailles florence.henry@obspm.fr

Introduction aux Systèmes et aux Réseaux

INTRODUCTION A JAVA. Fichier en langage machine Exécutable

Info0101 Intro. à l'algorithmique et à la programmation. Cours 3. Le langage Java

I. Introduction aux fonctions : les fonctions standards

Les structures. Chapitre 3

4. Outils pour la synchronisation F. Boyer, Laboratoire Lig

UE Programmation Impérative Licence 2ème Année

IRL : Simulation distribuée pour les systèmes embarqués

Cahier des charges. driver WIFI pour chipset Ralink RT2571W. sur hardware ARM7

J2SE Threads, 1ère partie Principe Cycle de vie Création Synchronisation

INITIATION AU LANGAGE JAVA

Les chaînes de caractères

Cours d Algorithmique et de Langage C v 3.0

Cours 6 : Tubes anonymes et nommés

Arguments d un programme

Initiation. àl algorithmique et à la programmation. en C

Tp 1 correction. Structures de données (IF2)

Cette application développée en C# va récupérer un certain nombre d informations en ligne fournies par la ville de Paris :

Introduction aux systèmes temps réel. Iulian Ober IRIT

TP : Gestion d une image au format PGM

Programme Compte bancaire (code)

Manuel d'installation

Claude Delannoy. 3 e édition C++

LMI 2. Programmation Orientée Objet POO - Cours 9. Said Jabbour. jabbour@cril.univ-artois.fr

Un ordonnanceur stupide

Plan du cours. Historique du langage Nouveautés de Java 7

Derrière toi Une machine virtuelle!

Programmer en JAVA. par Tama

EPREUVE OPTIONNELLE d INFORMATIQUE CORRIGE

TD2/TME2 : Ordonnanceur et Threads (POSIX et fair)

TP, première séquence d exercices.

Cours de C. Petits secrets du C & programmation avancée. Sébastien Paumier

Langage C. Patrick Corde. 22 juin Patrick Corde ( Patrick.Corde@idris.fr ) Langage C 22 juin / 289

Cours Langage C/C++ Programmation modulaire

Cours de Programmation Impérative: Zones de mémoires et pointeurs

Projet de programmation (IK3) : TP n 1 Correction

Java Licence Professionnelle CISII, Cours 2 : Classes et Objets

Info0604 Programmation multi-threadée. Cours 5. Programmation multi-threadée en Java

INF111. Initiation à la programmation impérative en C amini/cours/l1/inf111/ Massih-Reza Amini

Introduction : les processus. Introduction : les threads. Plan

Licence Bio Informatique Année Premiers pas. Exercice 1 Hello World parce qu il faut bien commencer par quelque chose...

Transcription:

Programmation multitâches LinuxThreads Labo-Unix - http ://www.labo-unix.net 2001-2002 1

TABLE DES MATIÈRES TABLE DES MATIÈRES Table des matières 1 Un peu de théorie... 3 1.1 Qu est-ce qu un thread?................................... 3 1.2 Atomicité........................................... 3 1.3 Volatilité............................................ 3 1.4 Verrous............................................ 4 2 LinuxThreads en pratique... 4 2.1 Un premier programme.................................... 4 2.2 Gestion des données partagées................................ 5 2.2.1 Les Mutex...................................... 5 2.2.2 Les sémaphores POSIX............................... 8 2.2.3 Variables de condition................................ 9 2

1 UN PEU DE THÉORIE... Introduction Ce document présente la bibliothèque de développement LinuxThreads crée par Xavier Leroy. C est une implémentation gratuite de la norme POSIX 1003.1c qui est censée assurer une certaine portabilité entre les différents systèmes d exploitation. POSIX 1003.1c n est malheureusement pas toujours supportée par les OS ; si Solaris y est totalement conforme, on ne peut pas en dire autant d autres OS même s il existe des projets visant à ajouter cette compatibilité (on peut remarquer notamment le projet Open Source Pthreads-Win32 qui apporte (enfin) un réel support de la norme dans Windows). LinuxThread est presque entièrement conforme, les seuls différences qu on peut trouver résident dans le nomage des fonctions et dans la gestion des signaux. Même si nous étudions ici une seule bibliothèque, l ensemble des notions et des techniques de programmation des threads que nous verrons pourra vous servir sur n importe quel système d exploitation. Cette méthode moderne de programmer est de nos jours la plus répandue et la plus pratique pour développer des applications rapides et complexes. 1 Un peu de théorie... Avant de commencer l étude des LinuxThreads nous allons expliquer le principe des threads et des notions qui y sont associées. 1.1 Qu est-ce qu un thread? Un thread peut être considéré comme une forme de mini-processus. Plusieurs mini-processus peuvent s executer en parallèle avec d autres dans un même programme. Les programmes utilisant les threads permettent, tout comme les programmes multi-processus, d échapper à l exécution séquentielle des instructions et ainsi de pouvoir accomplir plusieurs tâches à la fois. Mais il ne faut pas confondre avec la programmation multi-processus classique utilisant l appel fork(). Les threads eux, partagent tous le même espace en mémoire ainsi que les mêmes ressources (descripteurs de fichier, sockets etc...) contrairement aux processus classiques qui possèdent chacun leur propre espace mémoire. Plus besoin donc d avoir recours à un segment de mémoire partagée pour échanger des données, chaque thread peut accéder à toutes les variables d un programme. Ceci implique aussi que le changement de contexte entre deux threads est beaucoup moins gourmand en ressources que le changement de contexte entre deux processus. Un autre avantage des threads dans le cas la programmation sur des systèmes multi-processeurs, est l exécution en parallèle de ceux-ci sur chacun des processeur qui permet d exploiter au mieux ces systèmes. 1.2 Atomicité On va s apercevoir rapidement le partage des variables entre les threads comporte aussi des risques qu il faut prendre en compte si on ne veut assurer une certaine stabilité a nos programmes. Les opérations atomiques sont des instructions (assembleur par exemple) transparentes ne pouvant être interrompues. Les opérations sur les données en C ne sont pas forcément atomiques et cela va poser problème avec la mémoire partagée. Par exemple, une opération d addition a+=1 est composé de plusieurs instructions assembleur, une addition et une affectation. Entre ces deux instructions, un autre thread peut accéder à la donnée et la modifier, provoquant ainsi un résultat inattendu pour l opération. 1.3 Volatilité GCC va créer un problème qui s ajoute à celui de la non-atomicité des opérations... Il effectue des optimisations en plaçant temporairement les valeurs de variables partagées dans les registres du processeur pour effectuer des calculs. Les threads accédant à la variable à cet instant ne peuvent pas se rendre compte des changements effectués sur celle-ci car sa copie en mémoire n a pas encore été modifiée. 3

2 LINUXTHREADS EN PRATIQUE... 1.4 Verrous Pour lui éviter d effectuer ces optimisations, il faut ajouter le qualificatif volatile à la déclaration de tous les objets qui seront en mémoire partagée. 1.4 Verrous On a vu que le problème de non-atomicité des opérations pose un problème lors d un accès concurrent à une variable. Pour éviter ce problème, il faut pouvoir rendre atomiques les opérations sur les variables partagées. Pour cela ont étés mis en place des systèmes de verrous qui bloquent l accès à une variable (a une ressource) tant que l opération sur celle-ci n est pas achevée. Le principe est dès plus simple. Lorsqu on souhaite modifier une variable : - on pose un verrou sur celle-ci (les autres threads ne peuvent plus y accéder). - on effectue toutes les opérations qu on souhaite dessus (une ou plusieurs). - on retire le verrou (et la variable est de nouveau disponible pour les autres threads). Quand un verrou est déjà posé sur une variable et qu un thread souhaite y accéder, celui-ci pourra être bloqué tant que le verrou ne sera pas retiré. 2 LinuxThreads en pratique... 2.1 Un premier programme Les fonctions de manipulation de threads sont déclarées dans le fichier entête pthread.h. Nous allons étudier un premier programme utilisant deux threads d affichage qui effectuent chacun la même opération en parallèle (afficher la valeur d un compteur). thread1.c : #include <stdio.h> #include <stdlib.h> #include <pthread.h> void *fonction_thread (void * arg) int i; for (i = 0 ; i < 5 ; i++) printf ("%s thread: %d\n", (char*)arg, i); usleep(10); pthread_exit(0); int main (void) pthread_t th1, th2; void *ret; if (pthread_create (&th1, NULL, fonction_thread, "Premier") < 0) perror("premier (pthread_create)"); if (pthread_create (&th2, NULL, fonction_thread, "Second") < 0) perror("second (pthread_create)"); 4

(void)pthread_join (th1, &ret); (void)pthread_join (th2, &ret); return 0; On le compile en utilisant la commande suivante : gcc -D_REENTRANT thread1.c -lpthread Dans ce programme simple, on utilise trois fonctions de linuxthreads : pthread_create() - Cette fonction permet de créer et d associer un thread à une fonction. Ici c est la fonction d affichage fonction_thread() qu on a associé à chacun des threads. On peut aussi passer un argument de type void * (n importe quel type) à la fonction threadée. pthread_join() - sert à un thread ou au programme principal à attendre la fin d un thread. Le second argument de la fonction sera remplis avec la valeur de retour du thread. pthread_exit() - permet de terminer l exécution d un thread et envoie une valeur de retour. La fonction sleep() permet de mettre en évidence l exécution parallèle des deux threads. Sinon, le premier thread aurait le temps de se terminer avant même que le second ne soit crée... 2.2 Gestion des données partagées On a vu précédemment que les données partagées devaient être protégées lorsqu on y accédait afin d éviter le problème de non-atomicité des opérations. La méthode la plus simple pour placer un verrou et ainsi protéger une donnée s appelle Mutex. 2.2.1 Les Mutex Mutex vient de MUTual EXclusion. Leur gestion est des des plus simple puisqu elle consiste à utiliser deux fonctions : pthread_mutex_lock() - Pour placer le verrou. pthread_mutex_unlock() - Pour le retirer. Pour illustrer l utilité des Mutex nous allons imaginer un programme construit comme suit : - un premier thread va remplir un tableau avec une fonction très lente qui met une demi-seconde pour remplir une seule case (nous simulons une fonction complexe qui effectue de nombreux calculs pour obtenir les valeurs qu elle met dans les cases). - un second thread va lire le contenu du tableau avec une fonction très rapide qui lit l intégralité du tableau en moins d une demi-seconde. Sans utiliser de Mutex, le programme ressemblerait à ceci : #include <stdio.h> #include <stdlib.h> #include <pthread.h> volatile int tab[5]; // Variable partagée void *lire (void * arg) int i; for (i = 0 ; i!= 5 ; i++) printf ("Thread lecture: tab[%d] vaut %d\n", i, tab[i]); 5

void *ecrire (void * arg) int i; for (i = 0 ; i!= 5 ; i++) tab[i] = 2 * i; printf ("Thread ecriture: tab[%d] vaut %d\n", i, tab[i]); usleep(500000); /* Simule un calcul complexe... */ int main(void) pthread_t th1, th2; void *ret; if (pthread_create (&th1, NULL, ecrire, NULL) < 0) perror("thread ecrire (pthread_create)"); if (pthread_create (&th2, NULL, lire, NULL) < 0) perror("thread lire (pthread_create)"); (void)pthread_join (th1, &ret); (void)pthread_join (th2, &ret); Et sa sortie ressemblerait à ceci : root@tavarua ~-->./a.out <(1 :04 :24) Thread ecriture : tab[0] vaut 0 Thread lecture : tab[0] vaut 0 Thread lecture : tab[1] vaut 0 Thread lecture : tab[2] vaut 0 Thread lecture : tab[3] vaut 0 Thread lecture : tab[4] vaut 0 Thread ecriture : tab[1] vaut 2 Thread ecriture : tab[2] vaut 4 Thread ecriture : tab[3] vaut 6 Thread ecriture : tab[4] vaut 8 Le thread de lecture lit donc toutes le cases avant que le thread d écriture n ait le temps d écrire dans toutes les cases... C est assez génant. Voyons maintenant le même programme utilisant un Mutex pour gérer ceci : #include <stdio.h> #include <stdlib.h> #include <pthread.h> 6

volatile int tab[5]; // Variable partagée pthread_mutex_t mutex; void *lire (void * arg) int i; // pthread_mutex_lock(&mutex); for (i = 0 ; i!= 5 ; i++) printf ("Thread lecture: tab[%d] vaut %d\n", i, tab[i]); // pthread_mutex_unlock(&mutex); void *ecrire (void * arg) int i; pthread_mutex_lock(&mutex); for (i = 0 ; i!= 5 ; i++) tab[i] = 2 * i; printf ("Thread ecriture: tab[%d] vaut %d\n", i, tab[i]); usleep(500000); /* Simule un calcul complexe... */ pthread_mutex_unlock(&mutex); int main(void) pthread_t th1, th2; void *ret; pthread_mutex_init(&mutex, NULL); if (pthread_create (&th1, NULL, ecrire, NULL) < 0) perror("thread ecrire (pthread_create)"); if (pthread_create (&th2, NULL, lire, NULL) < 0) perror("thread lire (pthread_create)"); (void)pthread_join (th1, &ret); (void)pthread_join (th2, &ret); Désormais sa sortie est : root@tavarua ~-->./a.out <(1 :12 :55) Thread ecriture : tab[0] vaut 0 Thread ecriture : tab[1] vaut 2 Thread ecriture : tab[2] vaut 4 Thread ecriture : tab[3] vaut 6 Thread ecriture : tab[4] vaut 8 Thread lecture : tab[0] vaut 0 Thread lecture : tab[1] vaut 2 Thread lecture : tab[2] vaut 4 7

Thread lecture : tab[3] vaut 6 Thread lecture : tab[4] vaut 8 Lorsqu un thread tente de placer un verrou sur un mutex, si celui-ci est déjà placé, le thread se bloque jusqu a ce qu il puisse le placer à son tour. Dans notre exemple, le premier thread à être lancé est celui qui écrit dans le le tableau, c est donc lui qui place en premier le verrou et le second thread se retrouve bloqué lorsqu il essaye de placer le verrou. Les Mutex sont bien pratiques mais ils permettent juste de bloquer des threads en attendant une ressource, ils n est pas possible de spécifier l ordre dans lequel plusieurs threads peuvent accéder à celle-ci. Pour cela nous avons les fameux sémaphores... 2.2.2 Les sémaphores POSIX Vous vous souvenez des sémaphores System V qu on avait brièvement présenté dans le cours IPC? J avais dit un truc du style : les sémaphores System V c est pourrit donc on va pas se faire [bip] avec trop longtemps - enfin à peu près. Ca veut pas dire que le principe des sémaphore est mauvais, mais leur gestion à la norme System V est plus lente et gourmande en ressources que la version POSIX implémenté dans LinuxThreads. Les sémaphores sont purement et simplement des compteurs pour des ressources partagées par plusieurs threads. Le principe appliqué à la vie courante serait un grand magasin avec de nombreux clients et plusieurs caisses pour payer. Le nombre de caisses libres représente le compteur du sémaphore et les clients voulant payer représentent les thread souhaitant accéder à une ressource. Le compteur de sémaphore est positif temps qu il reste des caisses libres et lorsqu il est égal à 0, le client voulant payer doit attendre qu une caisse se libère. Pour accéder aux fonctions sur les sémaphores, il faut utiliser le fichier entête sémaphore.h en plus de pthread.h. int sem_init(sem_t *sem, int pshared, unsigned int valeur) ; Initialise le sémaphore pointé par sem. Le compteur associé au sémaphore est initialisé à valeur. L argument pshared indique si le sémaphore est local au processus courant (vaut 0) ou s il est partagé entre les plusieurs processus (ce dernier comportement n est pas encore géré par LinuxThreads). int sem_wait(sem_t *sem) ; Suspend le thread appelant la fonction jusqu a ce que le sémaphore pointé par sem ait une valeur non nulle. Lorsque le compteur devient non nul, le compteur du sémaphore est atomiquement décrémenté. int sem_trywait(sem_t *sem) ; C est une variante non bloquante de sem_wait(). Si le sémaphore pointé par sem est non nul, le compteur est décrémenté atomiquement et la la fonction retourne 0. Si le compteur du sémaphore est à 0, la fonction retourne EAGAIN. int sem_post(sem_t *sem) ; Incrémente atomiquement le compteur du sémaphore pointé par sem. Cette fonction n est pas bloquante. int sem_getvaleur(sen_t *sem, int *sval) ; Sauvegarde dans la variable pointée par sval la valeur courante du compteur du sémaphore sem. int sem_destroy(sem_t *sem) ; Détruit un sémaphore et libère toutes les ressources qu il possède. Dans LinuxThreads on ne peut pas associer de ressource à un sémaphore donc cette fonction ne fait que vérifier qu aucun thread n est bloqué sur le sémaphore. 8

2.2.3 Variables de condition Les condition variables (condvar) permettent de réveiller un thread endormis en fonction de la valeur d une variable. Par exemple, en reprenant le cas du magasin, on pourrait souhaiter qu a une certaine heure, les caisses ferment et que les clients ne puissent plus payer. On pourrait gérer ceci uniquement avec des sémaphores mais les condvar vont nous faciliter la tache. Attention! Il faut toujours protéger la variables d un condvar avec un mutex pour éviter les race conditions. Une race condition est le cas ou un thread se prépare à attendre une condition et un autre signale la condition juste avant que le premier n attende réellement. Car dans ce cas, le thread qui se met en attente pourrait ne jamais être réveillé. int pthread_cond_init(pthread_cond_t *cond, pthread_cond_attr_t *cond_attr) ; Initialise une la condvar cond en utilisant les attributs de condition spécifiés par cond_attr ou les attributs par défaut si cond_attr vaut NULL. cond_attr est pour l instant ignoré dans l implémentation Linux- Threads. Plus simplement, on peut initialiser les variables de type pthread_cond_t en utilisant la constante PTHREAD_COND_INITIALIZER. int pthread_cond_signal(pthread_cond_t *cond) ; Permet de relancer un thread attendant la condition cond. S il aucun thread n attend, il ne se passe rien, si plusieurs threads attendent sur la même condition, un seul d entre eux est réveillé mais il est impossible de prédire lequel. int pthread_cond_broadcast(pthread_cond_t *cond) ; Relance tous les threads qui attendent la condition cond. int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) ; Déverrouille le mutex et attend que la variable cond soit signalée. Le thread est endormis pendant ce temps. Le mutex doit être préalablement verrouillé par le thread. Lorsque la fonction rend la main, elle reverrouille le mutex. int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex, const str Le comportement est le même que pour la fonction précédente mais elle s effectue sur un laps de temps donné. int pthread_cond_destroy(pthread_cond_t *cond) ; Détruit une variable de condition. Sous Linux, cette fonction ne fait que vérifier qu aucun thread n attend la condition. Note sur la gestion des signaux asynchrones : Il ne faut pas utiliser ces fonction dans un signal handler car ces fonctions ne sont pas atomiques, cela peut placer un thread en position de deadlock (exclusion mutuelle avec lui-même). Exemple : #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> static sem_t my_sem; int the_end; void *thread1_process(void * arg) 9

while (!the_end) printf ("Je t attend!\n"); sem_wait (&my_sem); printf ("OK, je sors!\n"); void *thread2_process(void * arg) register int i; for (i = 0 ; i < 5 ; i++) printf ("J arrive %d!\n", i); sem_post (&my_sem); sleep (1); the_end = 1; sem_post (&my_sem); /* Pour debloquer le dernier sem_wait */ int main(void) pthread_t th1, th2; void *ret; sem_init (&my_sem, 0, 0); if(pthread_create (&th1, NULL, thread1_process, NULL) < 0) fprintf (stderr, "pthread_create error for thread 1\n"); if(pthread_create (&th2, NULL, thread2_process, NULL) < 0) fprintf (stderr, "pthread_create error for thread 2\n"); (void)pthread_join (th1, &ret); (void)pthread_join (th2, &ret); return 0; Exercice : Créez un programme qui simule l exemple précédement cité du grand magasin. On doit simuler un magasin qui comporte 5 caisses et 20 clients voulant payer un article (une FNAC?). Chaque payement prend 5 minutes (c est une FNAC...) que l on représentera dans le programme par 1 seconde. Le magasin ferme dans 15 minutes (6 secondes dans le programme), il faut que le programme donne le nombre de clients qui auront le temps de passer. (Bon, c est vrai, dans la réalité, ils feraient passer tout le monde... mais faut bien trouver un exemple. On a qu a dire que ce sont des guichets SNCF) 10