0% ont trouvé ce document utile (0 vote)
116 vues36 pages

5 Synchronisation

Le document présente la notion de thread et de multithreading, ainsi que le problème des accès concurrents à une variable partagée pouvant mener à une race condition. Il introduit le concept d'exclusion mutuelle permettant de garantir l'atomicité des sections critiques.

Transféré par

Mãřçèł Kankeu
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd
0% ont trouvé ce document utile (0 vote)
116 vues36 pages

5 Synchronisation

Le document présente la notion de thread et de multithreading, ainsi que le problème des accès concurrents à une variable partagée pouvant mener à une race condition. Il introduit le concept d'exclusion mutuelle permettant de garantir l'atomicité des sections critiques.

Transféré par

Mãřçèł Kankeu
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd

Multithreading et synchronisation

Guillaume Salagnac

Insa de Lyon – Informatique

2019–2020
Plan

1. Introduction : la notion de thread

2. Problème de l’exclusion mutuelle

3. Un mécanisme de synchro universel : le sémaphore

2/36
Rappel : la notion de processus
Définition : processus VPN

«un programme en cours 0


d’exécution» 1
• isolés les uns des autres 2
• en temps : CPU virtuel
• en espace : mémoire virtuelle
• Process Control Block
• numéro = PID
• environnement, répertoire
courant, fichiers ouverts...
• copie des registres CPU
• vue mémoire = Page Table
• Page Table
• instructions = .text
• variables globales = .data
• tas d’allocation = .heap
Virtual Address Space
• pile d’exécution = .stack
3/36
Notion de thread (VF fil d’exécution)
VPN
Définition : thread 0

«une tâche indépendante à 1


l’intérieur d’un processus» 2
• pourquoi les threads ?
• profiter de plusieurs CPU
• faciliter la programmation
• vue mémoire commune
• pas d’isolation matérielle
• variables globales partagées
• tas d’allocation commun
• ordonnancement indépendant
• un VCPU privé
TCB = Thread Control Block
• une pile d’exécution privée
• variables locales privées
Virtual Address Space
4/36
Notion de thread : remarques

• parfois appelé «processus léger» mais vision archaïque


• en vrai : un PCB = une PT et un/plusieurs TCB
• par ex : task_struct et mm_struct dans Linux
• un thread ne peut pas vivre en dehors d’un processus
• besoin d’une vue mémoire
• un processus vivant a toujours au moins un thread
• «main thread» = thread qui exécute main()
• lorsque zéro thread I processus terminé

5/36
Rappel : changement de contexte
Thread 1 Noyau Thread 2

T1 est actif
copier les
interruption registres CPU
vers TCB1
ou syscall
choisir T2
T2 est inactif
T1 est inactif ssi P1 6= P2:
vidanger cache
et adopter PT
charger les de P2
registres CPU
depuis TCB2 RETI T2 est actif

6/36
API POSIX : Threads
#include <pthread.h>

/* opaque typedefs */ pthread_t, pthread_attr_t;

// create and start a new thread


int pthread_create(pthread_t *thread,
pthread_attr_t *attr,
void * (*function) (void *),
void *arg);

// terminate the current thread


void pthread_exit(void *retval);
// terminate another thread
int pthread_cancel(pthread_t thread);

// wait for another thread to terminate


int pthread_join(pthread_t thread, void **retvalp);
7/36
Plan

1. Introduction : la notion de thread

2. Problème de l’exclusion mutuelle

3. Un mécanisme de synchro universel : le sémaphore

8/36
Accès concurrents à une variable partagée
Variable partagée
Thread A Thread B
int var = 5;
{ {
... ...
... ...
var = var+1; var = var-1;
... ...
... ...
} }

Question : que vaut var à la fin de l’exécution ?


• intuition : var==5
• réalité : var==5 ou var==4 ou var==6

9/36
Explication : code source 6= instructions du processeur

Variable partagée
Thread A Thread B
var: 00000005
... ...
... ...
LOAD REGa←[var] LOAD REGb←[var]
INCR REGa DECR REGb
STORE REGa →[var] STORE REGb →[var]
... ...
... ...

Remarque : A et B exécutés sur des (V)CPU distincts


I REGa et REGb (physiquement ou logiquement) distincts

10/36
Quelques exécutions possibles

TA var = 5 TB TA var = 5 TB TA var = 5 TB


LOAD REGa←[var] LOAD REGa←[var] LOAD REGa←[var]
INCR REGa context switch DECR REGb
STORE REGa→[var] LOAD REGb←[var] context switch
context switch DECR REGb LOAD REGb←[var]
LOAD REGb←[var] STORE REGb→[var] INCR REGa
DECR REGb context switch STORE REGa→[var]
STORE REGb→[var] INCR REGa context switch
STORE REGa→[var] STORE REGb→[var]
var = 5
var = 6 var = 4

Remarque : 1 CPU ou 2 CPU I problème semblable

11/36
Notion de «race condition»
VF «situation de concurrence», course critique, accès concurrents
Définition : race condition
Situation où le résultat du programme dépend de l’ordre dans
lequel sont exécutées les instructions des threads

Remarques
• plusieurs accès concurrents à une ressource partagée
• variable globale, fichier, réseau, base de données...
• écriture+écriture = problème
• écriture+lecture = problème
• concurrence : parallélisme et/ou entrelacement
• i.e. quand on ne maîtrise pas l’ordre temporel des actions
• risques : corruption de données et/ou crash

• mauvaise nouvelle : très difficile à débugger en pratique


• bonne nouvelle : des protections efficaces existent
12/36
Situation de concurrence : exemples

• deux écritures concurrentes = conflit


Thread A: x=10
Question : valeur finale de x ?
Thread B: x=20

• une lecture et une écriture concurrentes = conflit


Init: x=5
Thread A: x=10 Question : valeur affichée ?
Thread B: print(x)

Précepte : race condition = bug


Un programme dans lequel plusieurs tâches peuvent se retrouver
en situation de concurrence est un programme incorrect.

13/36
Objectif : garantir l’exclusion mutuelle
Définitions
• Action atomique : action au cours de laquelle aucun état
intermédiaire n’est visible depuis l’extérieur
• Ressource critique : objet partagé par plusieurs threads et
susceptible de subir une race condition
• Section critique : morceau de programme qui accède a
une ressource critique

Idée : on veut que chaque section critique soit atomique

Définition : exclusion mutuelle


Interdiction pour plusieurs threads de se trouver simultanément
à l’intérieur d’une section critique

Idée : «verrouiller» l’accès à une section critique déjà occupée

14/36
Exclusion mutuelle par verrouillage
Variables partagées
Thread A int var = 5; Thread B
lock_t L;
{ {
... ...
lock(L); lock(L);
var = var+1; var = var-1;
unlock(L); unlock(L);
... ...
} }

On voudrait ces deux méthodes atomiques :


• lock(L) pour prendre le verrou L en exclusivité
I un seul thread peut entrer en section critique
• unlock(L) pour relâcher le verrou L
I permet aux autres threads de le prendre à leur tour
15/36
Exclusion mutuelle : illustration

A entre en section critique A sort de la section critique

lock() unlock()
TA

lock() unlock()
TB temps









B attend
B veut entrer en B sort de la
B entre en
section critique section critique
section critique

16/36
Problème : comment garantir l’exclusion mutuelle ?
Autrement dit : comment implémenter lock() et unlock() ?
Propriétés souhaitables
• Exclusion mutuelle : à chaque instant, au maximum une seule
tâche est en section critique
• sinon risque de race condition
• Progression : si aucune tâche n’est en section critique, alors une
tâche exécutant lock() ne doit pas se faire bloquer
• sinon risque de deadlock, en VF interblocage
• Équité : aucune tâche ne doit être obligée d’attendre indéfiniment
avant de pouvoir entrer en section critique
• sinon risque de starvation, en VF famine, privation
• Généralité : pas d’hypothèses sur les vitesses relatives ou sur le
nombre des tâches en présence
• on veut une solution universelle
• Bonus : implem simple, algo prouvable, exécution efficace...

17/36
Solution naïve (et incorrecte)
partagé
int turn = 1;
Thread A Thread B
while(1) while(1)
{ ... { ...
while(turn==2) while(turn==1)
{/* attendre */ } {/* attendre */ }
// section critique // section critique
turn = 2; turn = 1;
... ...
} }

• Exclusion mutuelle : OK
• Attente active : exécution pas très efficace
• Problème : alternance stricte I progression non garantie
18/36
Solution naïve no 2 (incorrecte aussi)
partagé
Thread A bool occup = 0; Thread B
while(1) while(1)
{ ... { ...
while(occup != 0) while(occup != 0)
{/* rien */ } {/* rien */ }
occup = 1; occup = 1;
// section critique // section critique
occup = 0; occup = 0;
... ...
} }

• Progression : OK
• Exclusion mutuelle : non garantie
• Problème : consultation-modification non atomique
19/36
Solutions correctes
Masquer les interruptions
• idée : empêcher tout changement de contexte
I dangereux, et inapplicable sur machine multiprocesseur

Approche purement logicielle


• idée : programmer avec des instructions atomiques
• autrefois seulement LOAD et STORE I par ex. algo de Peterson
• de nos jours : TEST-AND-SET, COMPARE-AND-SWAP I spin-lock
I attente active = souvent inefficace à l’exécution

Approche noyau : intégrer synchronisation et ordonnancement


• idée : programmer avec des instructions atomiques
• mais les cacher dans le noyau (derrière des appels système)
I permet de bloquer / réveiller les threads au bon moment

20/36
Mutex : définition
Verrou exclusif, ou en VO mutex lock
• objet abstrait = opaque au programmeur
• deux états possibles : libre=unlocked ou pris=locked
• offre deux méthodes atomiques
• lock(L) : si le verrou est libre, le prendre
sinon, attendre qu’il se libère
• unlock(L) : relâcher le verrou,
c.à.d. le rendre libre à nouveau

Remarques
• lock() et unlock() implémentés comme appels système
• threads en attente = état BLOCKED dans l’ordonnanceur
• une file de threads suspendus pour chaque mutex
• invoquer unlock() réveille un thread suspendu (s’il y en a)
• attention : ordre de réveil non spécifié

21/36
API POSIX : Mutex locks

#include <pthread.h>

/* opaque typedefs */ pthread_mutex_t, pthread_mutexattr_t;

// create a new mutex lock


int pthread_mutex_init(pthread_mutex_t *mutex,
pthread_mutexattr_t *mutexattr);

int pthread_mutex_lock(pthread_mutex_t *mutex);


int pthread_mutex_unlock(pthread_mutex_t *mutex);

// attempt to lock a mutex without blocking


int pthread_mutex_trylock(pthread_mutex_t *mutex);

22/36
Exclusion mutuelle : en résumé
Notion de «race condition»
• plusieurs accès concurrents à une même variable
• accès non atomique I données incohérentes

Section critique
• morceau de code qu’on veut rendre atomique
I exécution nécessairement en exclusion mutuelle

Solution : utiliser un mutex lock


• lock(L);
/* section critique */
unlock(L);
• nécessite que tous nos threads jouent le jeu

23/36
Plan

1. Introduction : la notion de thread

2. Problème de l’exclusion mutuelle

3. Un mécanisme de synchro universel : le sémaphore

24/36
Scénario producteur-consommateur : introduction
Deux threads communiquent via une file FIFO partagée

P C

Producteur Consommateur
while(1) while(1)
{ {
item=produce(); item = fifo_get();
fifo_put(item); consume(item);
} }

Remarques :
• file = tampon circulaire de taille constante
• producteur doit attendre tant que la file est pleine
• consommateur doit attendre tant que la file est vide

25/36
Prod.-consomm. : solution naïve (et incorrecte)
partagé
item_t buffer[N];
int count=0;
Producteur Consommateur
int in = 0; int out = 0;
while(1) while(1)
{ {
item=produce() while(count == 0) {}
while(count == N) {} item = buffer[out];
buffer[in] = item; out = (out+1) % N;
in = (in+1) % N; count = count - 1;
count = count + 1; consume(item);
} }

Observation : ce programme a des bugs de synchronisation


I Question : comment corriger le problème ?
26/36
Producteur-consommateur : remarques
• buffer partagé de taille N (constante) initialement vide
• buffer circulaire : x%N = x modulo N
• fonctions produce() et consume() non pertinentes
• supposées «purement séquentielles» AKA inoffensives
• variable partagée count pour la synchronisation
• indique le nombre d’éléments actuellement dans le buffer
• variables in et out : non partagées
• exemple avec N=10 :
count = 7
in = 5 out = 8

N J O U R B O
0 1 2 3 4 5 6 7 8 9

Mauvaise nouvelle
Cette synchro est irréalisable avec seulement lock()/unlock()
27/36
Quelques scénarios classiques de synchronisation
Exclusion mutuelle

Producteur consommateur, en VO bounded buffer problem

P C

Boucle parallèle, en VO fork-join

Rendez-vous, en VO barrier

28/36
Notion de sémaphore (Edsger W. Dijkstra 1930–2002)

• objet abstrait = opaque au programmeur


• contient une variable entière k
• initialisée avec k>0 lors de la création du sémaphore
• contient une file d’attente de threads bloqués
• offre deux méthodes atomiques P() et V()

P(S) V(S)
S.k = S.k - 1; S.k = S.k + 1;
if( S.k < 0 ) if( S.k <= 0 )
{ {
/* suspendre le thread /* ré veiller l’un des
appelant, et le threads de la file
mettre dans la file d’attente de S */
d’attente de S */ }
}

29/36
Sémaphore : remarques
• mécanisme de synchronisation très polyvalent
• permet de résoudre tous les scénarios ci-dessus

• implem : instructions atomiques dans appel système


• file d’attente = état BLOCKED dans l’ordonnanceur
• attention : ordre de réveil non spécifié

• cas général parfois appelé «sémaphore à compteur»


• k > 0 I nombre de «jetons disponibles» = k
• k 6 0 I nombre de tâches en attente = −k
• «sémaphore binaire» si k initialisée à 1
• équivalent à un mutex : P()=lock() et V()=unlock()
• attention : aucun moyen de consulter la valeur de k

• propriétés souhaitables : sûreté, progression, équité,


généralité, simplicité, prouvabilité, performance...
30/36
Producteur-consommateur avec sémaphores
partagé
item_t buffer[N];
sem_t emptyslots=N;
sem_t fullslots=0;

Producteur Consommateur
in = 0; out = 0;
while(1) while(1)
{ {
item=produce() P(fullslots);
P(emptyslots); item = buffer[out];
buffer[in] = item; out = (out+1) % N;
in = (in+1) % N; V(emptyslots);
V(fullslots); consume(item);
} }

31/36
Sémpahores POSIX

#include <semaphore.h>
/* opaque typedef */ sem_t;

// semaphore initialization and destruction


int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);

// synchronization methods
int sem_wait(sem_t *sem); // wait = P = down
int sem_post(sem_t *sem); // post = V = up = signal

32/36
Semaphores : intuitions

k = −2
k=13 k=5 k=0

• P() = «essayer de prendre un jeton, me suspendre si aucun dispo»


• AKA wait, acquire, pend, down
• un example d’appel bloquant
• V() = «ajouter un jeton et peut-être réveiller un autre thread»
• AKA post, signal, release, post, up
• un exemple d’appel non-bloquant

33/36
Notion de «deadlock», en VF interblocage
Définition : interblocage, en VO deadlock
Situation dans laquelle deux (ou plusieurs) tâches concurrentes
se retrouvent suspendues car elles s’attendent mutuellement

I...pour toujours
Exemple :

Init: Semaphore X=1, Y=1


Thread A: P(X); P(Y); print("A"); Question : résultat ?
Thread B: P(Y); P(X); print("B");

Exemple de trace d’exécution menant à l’interblocage


1 Thread A : P(X)
2 Thread B : P(Y)
3 Thread A : P(Y) I bloque A
4 Thread B : P(X) I bloque B
34/36
Plan

1. Introduction : la notion de thread

2. Problème de l’exclusion mutuelle

3. Un mécanisme de synchro universel : le sémaphore

35/36
Threads et synchronisation : en résumé
Processus VS thread VS mémoire virtuelle
• unité d’ordonnancement = thread
• unité d’isolation mémoire = espace d’adressage virtuel
• un processus = un espace d’adressage + un/plusieurs threads

Exclusion mutuelle AKA mutex


• stratégie permettant d’éviter les «race conditions»
• mécanisme : méthodes lock() et unlock() atomiques

Sémaphore
• mécanisme de synchronisation universel
• P() = «essayer de prendre un jeton, me suspendre si aucun dispo»
• V() = «ajouter un jeton et peut-être réveiller un autre thread»

36/36

Vous aimerez peut-être aussi