Sie sind auf Seite 1von 43

Chap.

1 : La programmation multi-threadée
concurrente (en POSIX C)

Contenu :
- l’intérêt du multi-threading
- le problème de concurrence entre threads
- les techniques de synchronisation : mutexs, sémaphores et variables
de condition
Les threads (ou tâches ou fils d'exécution)
Concurrence ou Parallélisme ?
Exemples d’architectures multicoeur
suite
Pourquoi une application multitâche?

q
Pour réduire le temps d’exécution en exploitant plusieurs cœurs

exp1: trier d'un tableau de 106 éléments en utilisant n threads,

exp2 : déterminer si un entier n est premier ou non

q
Parce que l'application est de nature multitâche

exp.: robot qui fait au même temps la reconnaissance de formes,
l'évitement d'obstacles, …
En cas de besoin on peut limiter le blocage à un seul thread
au lieu de stopper toute l'application

q
Pour donner l’illusion d’une exécution parallèle de plusieurs
actions sur un seul cœur.
Exp. : création de 2 threads.
#include <pthread.h>
int main(void) {
pthread_t my_thread1, my_thread2 ;

printf("Hello à tous ") ;


pthread_create(&my_thread1, NULL, action1, 100);
printf("main lance le 2ème thread ") ;
pthread_create(&my_thread2, NULL, action2, 200);

for (i=0 ; i<100 ; i++)


printf("Hello_ENISo_ %d ", i)
}

void *action1(void *param) {


for (i=0 ; i< param ; i++)printf("Hello IA%d ", i)
}
void *action2(void *param){
for (i=0;i<param ;i++) printf("Hello Meca%d ", i)

Comparons le temps CPU d'un code série à celui d'un code
parallèle (voi exemple comparaionSerialParallel.c)

> time ./comparaionSerialParallel monoThread


10000000
> time ./comparaionSerialParallel multiThread
10000000 2

Le temps CPU consommé c'est le « user- time! ».


(att : dans le cas multiThread il faut diviser la valeur de « user-time »
par le nombre de cœurs alloués)
La fonction C (posix) pour créer des threads

#include <pthread.h>
int pthread_create(pthread_t * thread, pthread_attr_t
* attr, void *(*Action) (void *), void *arg);

La fonction renvoie une valeur de type int : 0 si la création a été réussie ou
une autre valeur si il y a eu une erreur.

Les paramètres :

Un pointeur vers une variable pthread_t, dans laquelle l'identifiant
du nouveau thread sera stocké ;

Un pointeur vers un objet d'attribut de thread. Cet objet contrôle les
détails de l'interaction du thread avec le reste du programme.

Un pointeur vers la fonction qui va être exécutée par le thread.

Un argument transmis à la fonction à exécuter.


Chaque thread d'un processus a un identifiant de thread:
l
La fonction pthread_self() renvoie l'identifiant du thread en cours.
8
Exp.: création de plusieurs threads

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void* action(void *arg);

int main(void){
int i; pthread_t thread[10];

for (i=0 ; i<10 ; i++)


pthread_create(&thread[i], NULL, action,
(void*)i);

for (i=0 ; i<10 ; i++)


pthread_join(thread[i], NULL);
}
Attendre la terminaison d’un thread

Problématique. Exécutons comparaionSerialParallel.c


sans les appels de de la fonction pthread-join ().

int pthread_join(pthread_t thread, void **retval);

thread: l'identifiant du thread à attendre


retval: un pointeur vers une variable qui recevra la valeur de
retour du thread s'étant terminé. Si on a pas besoin de cette valeur de
retour, on passe NULL comme second argument.
La concurrence entre les threads

Quels sont les risques avec la programmation multithread ?

1) L'ordre d'exécution des threads est non déterministe : dû à l'OS


et au nombre changeant de cœurs disponibles.
Exp PbMultithreadNonDeterministe.c

2) L'accès aux ressources partagées risque de poser un


problème :
Exp: incrémenter une même variable partagée.
PbMultithreadRessourcePartagee_Mutex.c )

Exp : deux threads ou plus ayant à exécuter une même partie de


code. (comme imprimer en ordre les tableaux triés par trois threads)
Le partage des ressources entre threads

q
La ressource partagée peut être :

une variable globale de type simple

une variable globale de type structure: exp. liste chaînée

une suite d'instructions

un fichier de données

une clé flash USB

q
Solution : Assuerer l'exclusion mutuelle aux ressources partagées à
l'aide des mutexs et des sémaphores
Espace mémoire propre au thread et espace partagé
Les MUTEXs (Mutual EXclusion)

q
Un MUTEX est un objet d’exclusion mutuelle qui sert à gérer l'accès
aux ressources partagées.
C'est l'équivalent d'un jeton de disponibilité
q
Déclaration d'un Mutex:
pthread_mutex_t my_mutex PTHREAD_MUTEX_INITIALIZER
q
Un thread verrouille un Mutex en appelant:
pthread_mutex_lock(my_mutex) ;

Si my_mutex était déjà verrouillé par un autre thread, alors


thread_mutex_lock bloque l'exécution et ne se termine que
lorsque le my_mutex est déverrouillé
q
Un thread déverrouille un mutex en appelant:
pthread_mutex_unlock(my_mutex)
Lorsque le mutex est déverrouillé, un seul des threads suspendus
Exp. : utilisation de Mutex pour protéger l'accès à une
variable partagée

pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;

void *action1(void *param) {


int i, *nombre = (int*)param;

for (i=0 ; i<*nombre ; i++){


pthread_mutex_lock(&myMutex);
somme++;
pthread_mutex_unlock(&myMutex);
}
}

(voir PbMultithreadRessourcePartagee_Mutex.c et
PbMultithreadRessourcePartageeFixed_Mutex.c)
Exercice

Et si on utilise une variable booléenne à la place de mutex pour faire un


accès exclusif à la variable globale somme !


Donnez le pseudo-code de cette solution.

Quels sont les deux inconvénients de cette solution ?
Utilisation du Mutex pour protéger une section de code
(demo MultithreadAccesFichier_S1_Mutex.c)

pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER;

void* Tri(void *arg);

int main(void){
int i; pthread_t thread[10];

srand(time(NULL));
for (i=0 ; i<10 ; i++)
pthread_create(&thread[i], NULL, Tri, (void*)i);

for (i=0 ; i<10 ; i++)


pthread_join(thread[i], NULL);
return 0;
}
suite
void *Tri (void *arg) {
int Tab[Taille], i,j, temp;
for(i = 0 ; i < Taille ; i++)
Tab[i] = rand()%80;//On tire un nombre entre 0, 80

for(i = 0 ; i < (Taille-1) ; i++)


for(j = i+1 ; j < Taille ; j++)
if (Tab[j] < Tab[i]) {
temp= Tab[i];
Tab[i]=Tab[j];
Tab[j]= temp;
}
Affichage(Tab) ;
}
Affichage (Tableau){
pthread_mutex_lock(&mutex);
for(i = 0 ; i < Taille ; i++)
printf( "%d ",Tableau[i]);
pthread_mutex_unlock(&mutex);
Exp. : utilisation de Mutex pour protéger une section de code
(MultithreadSectionCodePartage.c)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER;


void* ma_fonction_thread(void *arg);

int main(void){
int i; pthread_t thread[10];

srand(time(NULL));
for (i=0 ; i<10 ; i++)
pthread_create(&thread[i], NULL, action, (void*)i);

for (i=0 ; i<10 ; i++)


pthread_join(thread[i], NULL);
}
void* action (void *arg){
int num_thread = (int)arg;
int nombre_iterations, i, j, k, n;
nombre_iterations = rand()%8;
for (i=0 ; i<nombre_iterations ; i++) {
n = rand()%10000;
pthread_mutex_lock(&my_mutex);
printf("Le thread numéro %d commence son
calcul\n",num_thread);

for (j=0 ; j<n ; j++)


for (k=0 ; k<n ; k++)
{}

printf("Le thread numero %d a fini son calcul\n",


num_thread);

pthread_mutex_unlock(&my_mutex);
}

pthread_exit(NULL);
}
Pb d'interblocage dû aux MUTEXs

Position du problème: Soient M1, M2 deux Mutexs et Ta, Tb deux threads.

t0 : Ta verrouille le mutex M1
t1 : Tb verrouille le mutex M2
t2 : Ta tente de verrouiller le mutex M2
t3 : Tb tente de verrouiller le mutex M1

q
Un interblocage survient lorsque un ou plusieurs threads sont
bloqués en attendant quelque chose qui n'aura jamais lieu.
q
Un type simple d'interblocage peut survenir lorsqu'un même thread
tente de verrouiller un mutex deux fois d'affilé
Vérification non bloquante d’un mutex

pthread_mutex_trylock(mutex)

Appelée sur un mutex déverrouillé, elle verrouille le Mutex comme le ferait
pthread_mutex_lock et renvoie zéro.

Si le mutex est déjà verrouillé, elle ne sera pas bloquante et renvoie le
code d'erreur EBUSY.


Question: donner l’équivalent du code suivant en utilisant le trylock
au lieu du lock:
i++ ;
pthread_mutex_lock(&M1) ;
j++ ;
Les sémaphores (pour la synchronisation)

q
Un sémaphore est un compteur qui peut être utilisé pour synchroniser
plusieurs threads.

q
Un sémaphore est représenté par une variable de type sem_t :
sem_t S1 ;

q
Un sémaphore supporte deux opérations de base:

sem_wait(S1): décrémente la valeur du sémaphore d'une unité.
Mais si la valeur est déjà à zéro, l'opération est bloquante jusqu'à ce que
la valeur du sémaphore redevienne positive


sem_post(S1): incrémente la valeur du sémaphore d'une unité.

q
Tout sémaphore doit être initialisé à une valeur x :
sem_init(S1, 0 , x);. // 2ème paramètre tjrs zéro
Différence entre Mutex et Sémaphore

q
Seul le thread qui détient le Mutex peut le libérer, or le sémaphore
peut être incrémenté par un thread et décrémenté par un autre.
q
Un sémaphore peut être initialisé à zéro : aucun jeton au départ.
Exp.

sem_t sem1, sem2; {/* variable globale : s maphore */


void* ma_fonction_thread(void *arg);

int main (int argc, char **argv) {


int i; pthread_t thread[10];
if (argc != 2)
printf("Usage : %s nbthreadmax\(\backslash\)n",
argv[0]);

sem_init(&sem1, 0, atoi(argv[1]));
for (i=0 ; i<10 ; i++)
pthread_create(&thread[i], NULL, action, (void*)i);

for (i=0 ; i<10 ; i++) pthread_join(thread[i], NULL);

sem_destroy(&sem1);
}
void* action(void *arg)
{
int num_thread = (int)arg;
int nombre_iterations, i, j, k, n;
nombre_iterations = rand()%8+1;

for (i=0 ; i<nombre_iterations ; i++) {


sem_wait(&sem1);
printf("Le thread %d entre dans la section critique
\n", num_thread);
sleep(rand()%9+1);
printf("Le thread %d sort de la section
critique \n", num_thread);
sem_post(&sem1);
sleep(rand()%9+1);
}

pthread_exit(NULL);
}
Exemples d'utilisation de sémaphores:
- partage d'un pont de capacité limité
- partage de cinq ponts
- par de cinq ports USB,
- partage d'une connexion réseau
-etc.
Autres fonctions sur les sémaphores


sem_init ( ) : pour initialiser le sémaphore à une valeur initiale;

sem_destroy ( ): détruire le sémaphore à la fin

sem_trywait ( ): permettant une mise en attente non bloquante.

sem_getvalue ( ): permettant d'obtenir la valeur courante d'un
sémaphore.

Att. : Utiliser ce genre de méthode peut conduire à des conditions de


concurrence critique : un autre thread peut changer la valeur du
sémaphore entre l'appel de sem_getvalue et l'appel à une autre
fonction de manipulation des sémaphores.
Sémaphore pour contrôler l'ordre d'exécution
interthread

Position du problème:

th_x th_y
O11:x=2 O21:z=4
O12:a= f1(x) ; O22: b= a+z ;

O13:c= b+2 O23 :

Comment faire pour que O22 s'exécute après O12 ?

Solution

th_x th_y
O11: O21:
O12: a= f1() ; O22: sem_wait (S1) ;
O12_1: sem_post(S1) ; O22_1: b= a+3 ;
O13:c= b+2 O23 :
Les Variables “de condition” (pour la synchronisation)

Position du problème : un thread th_x met à jour un compte


bancaire et un thread th_y a pour objectif de génèrer une alerte
chaque fois que le compte devient négatif.

th_x th_y
Solde= ... ...
if solde < 0 print"Alerte";
???
suite

Une variable de condition sert à faire patienter un thread


jusqu'à ce qu'un événement future survienne dans un
autre thread.

th_x th_y
O11:solde= ... O21:
O12: if solde < 0 O22: cond_wait (c1) ;
O13 signal (c1) ; O23:print"Alerte";
O14: O24 :

Si O13 est exécutée avant O22 alors O23 ne serait pas exécutée à
moins que C1 serait généré encore une deuxième fois plus tard.
Différence entre sémaphore et variable de condition

1) sem_wait( M1) ne bloque pas le thread à tous


les coups.

pthread_cond_wait (C1)bloque le thread à tous


les coups en attente d'une occurrence futur du signal

2) plusieurs threads peuvent être en attente d'un


même signal C1 (notion de broadcast)

3) plusieurs threads peuvent émettre un même signal


C1
Opérations sur les variables “de condition”

/*Déclaration d'une variable de condition */


pthread_cond_t C1 = PTHREAD_COND_INITIALIZER;

/* th_x délivre le signal */


pthread_cond_signal (&C1);

/* th_x délivre le signal sous forme de broadcast*/


pthread_cond_broadcast (&C1)

/* th_y attend que la condition soit remplie */


pthread_cond_wait (&C1, &mutex);

Pourquoi le mutex ? (voir diapo suivant)


Opérations sur les variables “de condition”

/*Déclaration d'une variable de condition */


pthread_cond_t C1 = PTHREAD_COND_INITIALIZER;

/* th_x délivre le signal */


pthread_cond_signal (&C1);

/* th_x délivre le signal sous forme de broadcast*/


pthread_cond_broadcast (&C1)

/* th_y attend que la condition soit remplie */


pthread_cond_wait (&C1, &mutex);

Pourquoi le mutex ? (voir diapo suivant)


Fonctionnement de pthread_cond_wait (&C, &M)

- Qd cet appel est exécuté par un th_x, l’OS met le thread en état wait
sur deux points C et M, et l'OS retire le mutex M qui devient libre.

- Qd le signal C est généré, l’OS marque que le thread th_x n’est plus
en attente de C mais seulement de M (si M est déjà pris).

- Plus tard quand M devient libre, l'OS l'attribue soit à th_y ou à un


autre demandeur. pthread_cond_wait (&C1, &M) bloque le thread
puis libère le mutex M
Exp.

th_x th_y
O11:
O12: solde= .. O21 :...
O13: if solde < 0 { O22:pthread_mutex_lock(&M)
O14: pthread_mutex_lock(&M) O23: pthread_cond_wait (&C1,
O15: pthread_cond_signal(&C1) &M)
O16: pthread_mutex_unlock(&M) O24:pthread_mutex_unlock(&M)
} O25: printf (''alarme'')
O26:...
O17 :….

Si on considère que C1 est une variable globale partagée alors le mutex M


est justifié.
Exercice

-Donner toutes les exécutions possibles du code


précédent ?
Terminaison et annulation d'un thread

Un thread se termine de trois façons:

- En arrivant à sa fin.

- En appelant la fonction
int pthread_exit(void *retval)
ou return()

- Qd un autre thread appelle la fonction


int pthread_cancel(pthread id_thread).
Auto terminaison d'un thread : exit(), return() ou
pthread_exit () ?

Seul return() fait partie du langage C.


q
Dans le main ()
exit( int retval)
return (int retval)
Les deux sont très similaires pour C
q
Dans une fonction racine d'un thread
exit( int retval)
return (void *retval)
pthread_exit (void *retval)
q
Dans d'une fonction ordinaire()
exit( int retval)
return (void *retval )
pthread_exit (void *retval)

En général, pthread_exit() ne libère pas toutes les ressources et les


Annulation d'un thread par un autre

int pthread_cancel(pthread id_thread) :

q
Un thread peut contrôler si et quand il peut être annulé par un autre.
q
Un thread peut réagir de trois façons face à l'annulation:
 Il peut être annulable de façon asynchrone. C'est-à-dire il peut être
annulé à n'importe quel moment de son exécution.
 Il peut être annulable de façon synchrone. Le thread n'est annulé que
lorsqu'il atteint un certain point de son exécution.
 Il peut être impossible à annuler. Les tentatives d'annulation sont
ignorées.
Exp.: rendre une section critique non-annulable

Void * ActionThread (montant){


...
if solde[personneX]< montant
printf (« solde insuffisant)
else {
pthread_setcancelstate(PTHREAD_CANCEL_DESABLE ,
NULL )
solde[personneX]-= montant ;
solde[personneY]+= montant ;
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE ,
NULL )

}
L'annulation de thread

q
Pour interdire aux autres de l’annuler, un thread appelle la fonction:
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE , NULL )

q
Pour indiquer un point annulation, le thread appelle la fonction:
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE , NULL )

Tjs le second argument, s'il n'est pas NULL, est un pointeur vers une
variable qui recevra le type d'annulation précédemment supportée par
le thread