Vous êtes sur la page 1sur 6

2.3.

Outils de Synchronisation

Plusieurs outils de synchronisation ont été proposé : les verrous, les événements, les sémaphores,
les moniteurs, les régions critiques et d’autres.

2.3.1 Outils primitifs de synchronisation :


C’est outils ne permettent de résoudre que des problèmes particuliers de synchronisation, nous
présentons dans ce qui suit deux outils : les verrous et les événements :

2.3.1.1 Les verrous(en anglais lock)


Ce mécanisme prermet de résoudre l'exclusion mutuelle, c'est un objet système sur lequel deux
opérations sont définies :
• lock(v): permet au processus d'acquérir le verrou v s'il est disponible. S'il n'est pas
disponible, le processus est bloqué en attente
• unlock(v) permet au processus de libérer le verrou v qu'il possédait. Si un ou plusieurs
processus étaient en attente de ce verrou, un seul de ces processus est réactivé.
En tant qu'opérations systèmes, ces opérations sont indivisibles. Le système utilise le masquage des
interruptions ou bien les instructions spéciales atomic (TSL) pour garantir l'accés en exclusion
mutuelle au verrou.

On peut voir un verrou comme un objet ayant deux attributs: une variable booleaine state et une file
d'attente q. state peut être false(ouvert) ou true (fermer),
La file d'attente q contient les processus en attente devant un verrou fermé.

struct Lock 
{ boulean state=false; // etat du verrou, false:ouvert, true: fermer
                       //initialisé à false. 
  queue q; // file d’attente 

lock(Lock *V) 

if (V.state == false) {V.state = true;} 
else {bloquer­le­processus­et­l­ajouter­­a­la­file V.q} 
}

unlock(Lock *V) 
{
if (!V.q vide){débloqué­un­seul­processus­de­la­file V.q}
else {V.state = false;} 
}

Exclusion Mutuelle par verrou:

Lock v;

process p1;
{
lock(v);
/*SECTION CRITIQUE*/
unlock(v);
}
2.3.1.2 Les événements
L’événement est l’outil le plus primitif permettant de résoudre le problème de synchronisation. Il
permet à un processus d'attendre la survenu de n'importe quelle evenement comme l'arrivé d'un
message par exemple.
Un événement peut avoir eu lieu ou ne pas avoir eu lieu (initialement l’événement n’a pas eu lieu).
Il est doté d'une liste d’attente contenant les éventuels processus en attente de l’événement. Un
évènement peut être manipulé grâce aux procédures wait, set, et clear définies comme suit :
wait(e):lorsqu'il est exécuté, provoque la suspension du processus en cours d'exécution jusqu'à ce
que l'état de l'événement e.happen soit vrai. Si l'état est déjà vrai (e.happen=true) avant l'appel de
wait, wait n'a aucun effet.
set(e)définit l'état de l'événement à vrai, libère tous les processus en attente.
clear(e) définit l'état de l'événement à faux.

Nous pouvons illustré l'evenement par l'implantation suivvante:

struct event 
{ boulean happen=false; // etat de l'evenement: true:survenu, false:non survenu
                        //initialisê à false
  queue q; // file d’attente 

wait(event *e) 

if (!e.happen){bloquer­le­processus­et­l­ajouter­a­la­file e.q} 
}

set(event *e) 
{
e.happen=true;
débloquer­tous­les­processus­dans­la­file e.q;
}
clear(event *e)
{
e.happen=false;
}

L'exemple suivant permet de synchroniser deux processus, le premier (sender) envoi un message au
second (receiver), ce dernier doit attendre la reception du message.

event e ; Process receiver :


message m ; …
wait(e) ;
Process sender : receive(m) ;
...
send (m) ;
set(e) ;
..
2.3.2 Les Sémaphores
Pour contrôler les accès à un objet partagé, E. W. Dijkstra (1965) suggéra l’emploi d’un nouveau
type de variables appelées sémaphores. Un sémaphore est un compteur entier qui désigne le nombre
d’autorisations d’accès disponibles. Chaque sémaphore a un nom et une valeur initiale. Les
sémaphores sont manipulés au moyen des opérations :
- P (désigné aussi par down ou wait) et
- V (désigné aussi par up ou signal).

L’opération P(S) décrémente la valeur du sémaphore S si cette dernière est supérieure à 0. Sinon le
processus appelant est mis en attente.
L’opération V(S) incrémente la valeur du sémaphore S, si aucun processus n’est bloqué par
l’opération P(S). Sinon, l’un d’entre-eux sera choisi et redeviendra prêt.
Chacune de ces deux opérations doit être implémentée comme une opération indivisible.

2.3.2.1 Implémentation des sémaphores

struct Semaphore 
{ int count; // valeur du sémaphore 
  queue q; // file d’attente 

P(Semaphore *S) 
{  
  S.count­= 1;
  if (S.count < 0) {bloquer­le­processus­et­l­ajouter­­a­la­file V.q} 

V(Semaphore *S) 

  S.count++ ;
  if (S.count <=0){débloqué­un­seul­processus­de­la­file V.q}

NOTES :
1- Le système garanti que les deux fonctions P() et V() sont exécutées en exclusion mutuelle en
utilisant le masquage des interruptions en monoprocesseur. En multiprocesseur, la solution est
l'utilisation de l’instruction Test and Set Lock.
2- Le sémaphore doit toujours être initialisé lors de sa création.
3- L'attribut S.count d'un Sémaphore ne peut être accédé pendant l’exécution.
4- Nous donnons des noms aux sémaphores selon leur utilisation :
- Sémaphore privé : S.count initialisé à 0 ;
- Sémaphore d’exclusion mutuelle : S.count initialisé à 1;
- Sémaphore général : S.count initialisé à une valeur N>0 ;
5- Si S.count <0, alors le nombre de processus bloqué dans la file S.q est égale à |S.count|

2.3.2.2 Exclusion mutuelle au moyen de sémaphores


Les sémaphores initialisé à 1 permettent d’assurer l’exclusion mutuelle :

semaphore mutex = 1 ; 

Processus P1 :  Processus P2 : 
{  { P(mutex) 
P(mutex)  Section_critique_de_P2(); 
Section_critique _de_P1() ;  V(mutex) ; 
V(mutex) ;  } 

2.3.2.3 Intérêt et inconvénient des sémaphores :
le sémaphore est mécanisme simple meilleur que l'attente active et peu coûteux. Généralement les
sémaphores sont implanté au niveau noyau système. Leur utilisation est un peut difficile, Cela est
du a la possibilité de tomber facilement dans un problème appelé interblocage définie comme suit :
soit deux processus P et Q désirant entrer en même temps dans deux sections critiques imbriquées.
semaphore mutax1, mutex2 ; le processus Q execute :

le processus P execute : P(mutex2) ;


P(mutex1) ;
P(mutex1) ; /*section critique*/
P(mutex2) ; V(mutex1) ;
/*section critique*/ V(mutex2) ;
V(mutex2) ;
V(mutex1) ;

.
Si une interruption horloge se produit juste après que le processus P exécute l'instruction
P(mutex1), le processus Q exécute P(mutex2) et se bloque dans P(mutex1). Le processus P se
bloque aussi dans P(mutex2). Les deux processus seront bloqués infiniment. On appel cette
situation : interblocage. Ce problème sera discuter dans le dernier chapitre de ce cours.

2.3.3. Les Régions Critiques


Avec les sémaphores, il est souvent difficile de s'assurer que le programme est correct. Afin de
faciliter la spécification de la synchronisation, Hoare a proposer l'utilisation des régions critiques.
Les régions critiques sont des outils de synchronisation de haut niveau. Elles impliquent l'utilisation
d'un type nommé shared et d'une instruction nommée region.
La déclaration des variables partagées doit être dans structure déclaré "shared" comme suit :

Shared Struct  data
{
/* declaration des variables partagées*/
}

Cela signifie que la variable peut être utilisée par plusieurs processus (mais pas en même temps).

L’accès aux variables partagées data doit toujours être dans un bloque ayant la structure syntaxique
suivante :

region Data do{/*section critique utilisant les variables
déclarées dans data*/};

region est un mot clé. data définit les données communes utilisées par la section critique.
L'instruction region permet donc une programmation plus structurée. La section critique doit être
définie comme un bloc d'instruction à l'intérieur de l'instruction region. Une erreur de l'usager sera
alors détectée par le compilateur. L'instruction region assure qu'un seul processus exécute la section
critique à la fois (c’est une garantie pour le programmeur).

Pour bloquer des processus en attente d'une condition, Hoare a proposer l'instruction suivante :

Region data when B do S; 
data est une variables déclarée shared, B est une expression booléenne et S une section de code
critique.Cela signifie : quand un processus essaye de rentrer dans la région, l’expression boolienne
B est évaluer, si l’expression est vrai alors l’instruction S est exécuter. Si elle est fausse, le
processus abandonne l’exclusion mutuelle et il est bloqué jusqu’à ce que B devienne vraie et
qu’aucun autre processus ne se trouve dans la zone associée à data. Chaque fois qu’un processus
quitte une région critique, tous les processus bloqués en attente de la condition B doivent se
réveiller pour réévaluer la condition B. ce qui est très coûteux sur le plan performances.
L’expression B est réévaluée par tout les processus en attente à chaque fois qu’un processus
abandonne la région critique.
A noté aussi que les régions critiques sont implantées au niveau bibliothèque. Le compilateur traduit
tout bloque de région critique par des sémaphores en insérant des P() et des V() sur les sémaphores
de tel sorte que la condition B soit toujours respecter et les variables déclaré dans data seront
accédés en exclusion mutuelle.

Attention :interblocage possible lors de l’utilisation des régions imbriquées


Un processus P1 exécute :region v1 do region v2 do S1;
Un processus P2 exécute :region v2 do region v1 do S2;

2.3.4 Les Moniteurs

Le mécanisme des moniteurs est un outil évolué de synchronisation proposé par Hoare &
Brinch.Hansen 1974, il fournit un mode d’expression de la synchronisation plus structuré que les
sémaphores, en vue de faciliter la compréhension et l’écriture des schémas de synchronisation, de
donner à ces schémas une forme synthétique.
Un moniteur est un module de programme,comportant des variables d’état internes et des
procédures externes (ou points d’entrée) accessibles depuis l’extérieur. Un moniteur est une
structure partagée entre plusieurs processus, qui se synchronisent en appelant ses points d’entrée
(les procédures) ;les variables d’état sont privés et ne sont pas directement accessibles. Les
procédures du moniteur contiennent des opérations qui permettent de bloquer ou de réveiller des
processus qui utilisent le moniteur. Une procédure d’initialisation (constructeur) est exécutée une
seule fois lors de la création du moniteur. Les constructions de synchronisation s’expriment à l’aide
d'un nouvau type appelée conditions. Une condition c est déclarée comme une variable, mais ne
peut être manipulée qu’au moyen trois primitives
Soit p désigne le processus appelant :

- wait(c) : bloque le processus p, et le place dans la situation : « en attente de c »


- signal(c) : if (!empty(c) ){ réveiller un des processus en attente de c }.
- empty(c) : retourne une valeur booléenne (vrai si aucun processus n’est en attente de c, faux
sinon)

Les variables d’état du moniteur doivent être manipulées en exclusion mutuelle pour garantir leur
cohérence. Lorsqu’un processus P réveille un processus Q par l’exécution de signal, le processus p
doit donc se bloquer momentanément pour assurer cette exclusion mutuelle, jusqu’au blocage ou à
la sortie de Q. Un processus qui est ainsi temporairement bloqué après avoir exécuté signal
bénéficie d’une priorité pour la reprise de son exécution, en vue d’éviter un blocage indéfini.

À l'intérieur des moniteurs


• des variables d'état : manipulables par les procédures externes seulement (encapsulation)
• des procédures internes : manipulées par les procédures externes seulement.
• des procédures externes : c’est les points d’entrée.
• des conditions : manipulable par les primitives de synchronisation.
• des primitives de synchronisation : wait, signal et empty.
• chaque moniteur possède une file d'attente globale.
• chaque variable condition référence une file d'attente.
• wait(c): placer le processus dans la file d'attente associée à une condition c.
• signal(c): sortir le processus suivant de la file d'attente associée à c.
• empty(c): teste si la file d'attente associée à c est vide.

File d'attente
de la
File d'attente
condition c1
de la
condition c1

Condition c1, c2;


Int x,y.
public f(){
.
.
.wait(c1)

}
File d'attente
du moniteur
public g(){
.
.signal(c1)
.
}
M(){
/*constructeur*/
}

Remarque : Un moniteur est en général implémenté avec des sémaphores, c'est tout à fait logique,
un système est bâti par niveaux d'abstraction successifs, un niveau i étant implémenté par les
primitives du niveau i-1.

Exemple : Exclusion mutuelle par Moniteur : Problème du compte bancaire

monitor Compte {
 int solde :; //variable manipulable seulement par des procédures du moniteur 
 public void  Ajouter(x) ; 
  {
    solde +=x ;
  }
 public void retirer(x) ;
  {
   solde ­=x ;
  } 
 Compte() // procédure d’initialisation 
  {
   solde=0 ;
  }
}

Vous aimerez peut-être aussi