• Aucun résultat trouvé

1.4. Coordination les processus

1.4.3 Les moniteurs

Qu’est-ce qu’un moniteur?

Un moniteur est un objet encapsulant des fonctions membre – ou des méthodes – dont une seule peut s’exécuter à un moment donné. La structure de moniteur permet d’éviter certains phénomènes d’interblocages qui peuvent apparaître avec les sémaphores. Ces interblocages sont généralement la conséquence d’erreurs de programmation vicieuses et difficiles à détecter. C’est pourquoi, on considère que l’écriture de sémaphores est délicate. La déclaration d’un moniteur est beaucoup plus facile et plus élégante par contre.

monitor MonMoniteur{ char tampon[100];

void écrivain(char *chaîne) {} char *lecteur() {}

}

Dans l’exemple précédent, en pseudo-C, quel que soit le nombre de processus qui s’exécutent et qui font appel au moniteur, seul un exemplaire de écrivain ou de lecteur pourra s’exécuter à un moment donné.

Dans le modèle de Hoare, les moniteurs disposent en plus d’un mécanisme qui permet à un processus de s’arrêter sur une condition particulière, de laisser sa place et de reprendre son exécution quand la condition est remplie. On arrête un

processus par wait(condition) et on le relance par notify(condition).

monitor MonMoniteur{ char tampon[100];

void écrivain(char *chaîne) { …

if (condition == false) wait(condition); … } char *lecteur() { … notify(condition); … } }

Un processus exécutant écrivain() avec condition à faux, s’arrêtera et laissera sa place, permettra à un autre processus d’exécuter une fonction du moniteur et attendra jusqu’à ce qu’un processus exécutant lecteur() le relance par notify().

Synchroniser les fils d’exécution

Les fils d’exécution s’exécutent dans le même espace mémoire. Ils peuvent tous accéder aux variables communes d’une classe simultanément. Comme pour les processus, ça peut conduire à des effet indésirables. Le langage Java ne dispose pas de sémaphores qu’il remplace par des moniteurs.

En Java, tout héritier de la classe Object est un moniteur potentiel – et donc toute classe, car Object est la classe mère –. Pour réaliser une exclusion et assurer un accès unique à une méthode, on utilise le mot clé

Systèmes d’exploitation & Réseaux informatiques

synchronized. Le compilateur associe un « moniteur » à chaque objet qui possède des variables ou des méthodes précédées de synchronized. Il supervisera un accès unique soit au niveau d’un objet de la classe, soit au niveau de la classe elle-même si elle est statique (static). Ainsi un seul fil de commande pourra exécuter une même méthode synchronisée à un moment donné. Et une seule des méthodes synchronisées d’un objet pourra s’exécuter. On peut aussi déclarer un bloc {… } comme synchronized sur un objet, par exemple

synchronized (monObjet) { // les intructions

}

Lorsqu’une méthode s’exécute sans avoir de données disponibles, elle peut bloquer les autres ou abandonner l’exécution sans avoir rien fait. Pour éviter ceci, on dispose de la fonction wait(), mais sans variable. Une fonction qui exécute ce wait()va alors s’arrêter, libérer le moniteur et attendre. Une autre méthode pourra prendre sa place.

Une fonction qui a exécuté un wait() pourrait rester bloquée à l’infini. C’est pourquoi, on dispose de la fonction notify() qui permet de relancer une méthode bloquée sur un wait() (n’importe laquelle). Une fonction qui exécute un notify() relancera une et une seule des fonctions en attente. notifyAll() permet de relancer toutes les méthodes bloquées par un wait().

Un exemple

Grâce aux moniteurs, on peut facilement implanter le programme de producteurs et de consommateurs. Pour ceci, les fils d’exécution les représentant doivent partager un tampon commun où les fils producteurs viendront écrire des messages et les fils consommateurs viendront les lire.

On peut réaliser ceci par une classe Tampon qui dispose de fonctions put() et get() synchronisées qui respectivement écrivent et lisent les messages : • put() écrit dans le tampon. S’il y a quelque chose, il attend qu’il se vide. • get() lit la valeur du tampon et le met à zéro. S’il n’y a rien, il attend qu’il se

remplisse.

class Tampon {

private String data = null;

public synchronized boolean put(String put_data) { // Si quelqu’un a écrit quelque chose

// on attend

while (data != null) { try {

wait();

} catch (InterruptedException e) {} }

data = put_data;

// Les autres peuvent se réveiller notifyAll();

return true; }

public synchronized String get() { while (data == null) {

try {

wait();

} catch (InterruptedException e) { } }

String get_data = data; data = null; notifyAll(); return get_data; } }

1.5. Ordonnancement

Systèmes d’exploitation & Réseaux informatiques

L’ordonnancement règle les transitions d’un état à un autre des différents processus. Cet ordonnancement a pour objectifs :

1. de maximiser l’utilisation du processeur; 2. d’être équitable entre les différents processus; 3. de présenter un temps de réponse acceptable; 4. d’avoir un bon rendement;

5. d’assurer certaines priorités;

1.5.1. Le tourniquet

Cet algorithme est l’un des plus utilisés et l’un des plus fiables. Chaque processus prêt dispose d’un quantum de temps pendant lequel il s’exécute. Lorsqu’il a épuisé ce temps ou qu’il se bloque, par exemple sur une entrée-sortie, le processus suivant de la file d’attente est élu et le remplace. Le processus suspendu est mis en queue du tourniquet.

Le seul paramètre important à régler, pour le tourniquet, est la durée du quantum. Il doit minimiser le temps de gestion du système et cependant être acceptable pour les utilisateurs. La part de gestion du système correspond au rapport de la durée de commutation sur la durée du quantum. Plus le quantum est long plus cette part est faible, mais plus les utilisateurs attendent longtemps leur tour. Un compromis peut se situer, suivant les machines, de 100 à 200 ms.

Documents relatifs