1 / 72

Chapitre 6 : Synchronisation des processus et des fils

Chapitre 6 : Synchronisation des processus et des fils. Introduction La section critique Matériel spécifique Sémaphores Problèmes classiques de synchronisation Moniteurs. Introduction. L’accès concurrent aux données peut conduire a des résultats irrationnels

step
Download Presentation

Chapitre 6 : Synchronisation des processus et des fils

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Chapitre 6 : Synchronisation des processus et des fils • Introduction • La section critique • Matériel spécifique • Sémaphores • Problèmes classiques de synchronisation • Moniteurs

  2. Introduction • L’accès concurrent aux données peut conduire a des résultats irrationnels • Il faut prévoir des mécanismes pour l’exécution ordonnée des processus qui coopèrent • Les “courses” aux ressources communes. • Macro-exemple : l'accès concomitant aux mêmes données dans plusieurs programmes.

  3. Gestion du solde d'un client Client C Enregistrement de règlements Prise de commande (1) Mise à jour compte (3) S = S - R S Validation du client (2) (2) Refus OK Enregistrer la commande Mise à jour compte (4) S = S + C

  4. Les courses • L’utilisation de la mémoire partagée pour résoudre le problème du producteur / consommateur (ch.4) contient une possibilité de course sur la donnée count.

  5. Les courses Les appels producteurs while (true) { while (count == BUFFER_SIZE) sommeil() ; // ne rien faire, le tampon est plein // le tampon devient disponible : on produit produit_suivant buffer[in] = produit_suivant; in = (in + 1) % BUFFER_SIZE; count++; }

  6. Les courses (suite) Les appels consommateur while (true) { while (count == 0) sommeil() ; // ne rien faire, le tampon est vide // le tampon contient quelque chose… consommé_suivant = buffer[out]; out = (out + 1) % BUFFER_SIZE; count--; // consomme l’élément consommé_suivant }

  7. Les courses (suite) • On peut programmer count++ ainsiregistre1 = count registre1 = registre1 + 1 count = registre1 • On peut programmer count-- ainsi registre2 = count registre2 = registre2 - 1 count = registre2 • Soit maintenant la séquence suivante d’exécution concomitante des deux fils ,(count étant une variable en mémoire et R1, R2 deux registres différents) : S0: le producteur exécute registre1 <= count //registre1 = 5S1: le producteur exécute registre1 <= register1 + 1 //registre1 = 6 S2: le consommateur exécute registre2 <= count //registre2 = 5 S3: le consommateur exécuteregistre2 <= registre2 - 1 //registre2 = 4 S4: le producteur exécute count <= registre1 //count = 6 S5: le consommateur exécute count <= registre2 //count = 4

  8. La section critique • Une section critique du processus (ou fil) P est constituée par une séquence d’instructions qui modifient l'état de ressources communes (avec d'autres processus ou fils). • Pour éliminer les courses, aucun processus concurrent ne doit exécuter une section critique pendant que P exécute les instructions d'une section critique (un processus concurrent est un processus qui fait appel a une ressource utilisée par P dans la section critique). • Les critères qui régissent le fonctionnement correct de processus (ou fils) qui ont des sections critiques sont au nombre de 3 : • Exclusion mutuelle dans le temps • Le choix du processus qui exécutera la section critique parmi plusieurs demandeurs doit se faire seulement entre les processus concurrents (les autres processus ne participent pas au choix) • Continuité : tous les processus doivent exécuter les sections critiques (pour éviter que certains restent en état d'attente indéfinie)

  9. Solution pour le cas de 2 tâches • Les deux tâches s’appellent T0 et T1 • Les instructions load, store et test sont “atomiques” • On présente 3 solutions qui utilisent la même interface ExclusionMutuelle public interface ExclusionMutuelle { public static final int TOUR_0 = 0; public static final int TOUR_1 = 1; public abstract void solliciteSectionCritique(int tour); public abstract void quitteSectionCritique(int tour); } (interface désigne en Java des objets abstraits avec méthodes et champs qui doivent être matérialisés en classes à l’aide du mot clé « implement » )

  10. Le programme TestAlgorithmes Ce programme crée 2 fils utilisés pour tester les algorithmes : public class TestAlgorithmes { public static void main(String args[]) { ExclusionMutuelle alg = new Algorithm_1(); Thread first = new Thread( new usine("usine 0", 0, alg)); Thread second = new Thread(new usine("usine 1", 1, alg)); first.start(); second.start(); } }

  11. Le fil usine public class usine implements Runnable { private String nom; private int id; // un nombre entier private ExclusionMutuelle mutex; public usine(String nom, int id, ExclusionMutuelle mutex) { this.nom = nom; this.id = id; this.mutex = mutex; } public void run() { while (true) { mutex.solliciteSectionCritique(id); Exemple.SectionCritique(nom); mutex.quitteSectionCritique(id); Exemple.SectionNormale(nom); } } }

  12. Algorithme 1 public class Algorithm_1 implements ExclusionMutuelle { private volatile int tour; public Algorithm 1() { tour = TOUR_0; } public void solliciteSectionCritique(int t) { while (tour != t) // ce n’est pas mon tour…. Thread.yield(); // laisse la place à un autre fil } public void quitteSectionCritique(int t) { tour = 1 - t; // si c’etait le tour de 1 le suivant sera 0, et inversement } }

  13. Algorithme_1 - explications • Les fils se partagent la variable entière tour. Cependant, le partage de cette variable n’est pas sans poser problème a cause des optimisations automatiques qu’un compilateur peut effectuer, comme par exemple lorsqu’il utilise le cache pour stocker une variable du programme qui ne change pas pendant plusieurs cycles d’UC (tour dans la boucle). C’est la raison de l’utilisation du qualificatif volatile • Si tour == i, le fil i peut continuer (Exemple.SectionCritique...) • Sinon, le fil s’arrête et annonce (Thread.yield()) qu'il peut temporairement céder l'accès à l'UC pour permettre à l’autre fil d'en prendre le contrôle. • Problème de continuité : L’alternance des fils est stricte et obligatoire - seulement le fil pour lequel tour == i pourra entrer dans la section critique, même si l’autre fil est dans une section non-critique. Ce qui peut conduire un fil à une attente infinie...

  14. Algorithme_2 • On ajoute de l’information pour indiquer l’intention du fil d’entrer dans une section critique, à l’aide de 2 variables booléennes (fanions), initialisées à FAUX • Le fil annonce son intention en changeant la valeur du fanion qui lui est associé en VRAI ; avant de pouvoir entrer dans sa section critique, l’autre fil doit avoir positionné le fanion à FAUX (annoncer l’intention de quitter la section critique). • Le critère de la continuité n’est toujours pas satisfait : si un changement de contexte intervient après le positionnement du fanion par un fil, mais avant que ce fil entre dans la boucle While, et que le deuxième fil (qui a provoqué le changement de contexte) positionne son fanion à VRAI, chacun des 2 fils attendra l’autre…

  15. Algorithme_2 public class Algorithm_2 implements ExclusionMutuelle { private volatile boolean flag0, flag1; public Algorithm_2() { flag0 = false; flag1 = false; } public void solliciteSectionCritique(int t) { if (t == 0) {flag0 = true; while(flag1 == true) Thread.yield(); } else {flag1 = true; while (flag0 == true) Thread.yield(); } } public void quitteSectionCritique(int t) { if (t == 0) flag0 = false; else flag1 = false; } }

  16. Algorithme_3 • Combine les idées de 1 et 2 • Satisfait-il aux critères de la section critique ?

  17. Algorithme_3 public class Algorithm_3 implements ExclusionMutuelle { private volatile boolean flag0; private volatile boolean flag1; private volatile int turn; public Algorithm_3() { flag0 = false; flag1 = false; turn = TURN_0; } // Suite…

  18. Algorithme_3 (suite) public void solliciteSectionCritique(int t) { int filno = 1 - t; turn = filno; if (t == 0) { flag0 = true; while(flag1 == true && turn == filno) Thread.yield(); } else { flag1 = true; while (flag0 == true && turn == filno) Thread.yield(); } } // Suite

  19. Algorithme_3 (suite et fin) public void quitteSectionCritique(int t) { if (t == 0) flag0 = false; else flag1 = false; } } // fin

  20. Spécifications matérielles destinées à la synchronisation des processus • Mono-processeurs – désactivation des interruptions • Le code en cours (section critique) peut s’exécuter sans être interrompu • Sur les systèmes multi-processeurs la solution est inefficace, car la désactivation et la réactivation des interruptions doit être appliquée à tous les processeurs, ce qui peut être très coûteux en temps de traitement. Dans ces systèmes il est en plus très difficile de prendre en compte des chagements de configuration (ajout de processeurs) • Utilisation des instructions “atomiques” (indivisibles et non-interruptibles) • Instructions TAS (Test-And-Set) • Instructions CAS (Compare-And-Swap) Ces instructions sont maintenant répandues dans les architectures multiprocesseurs car elle sont indipensables pour la réalisation de SE fiables. • Les instructions atomiques du niveau matériel ne sont généralement pas à la portée des programmeurs d’applications.

  21. Simulation de l’équipement (mémoire et instructions atomiques) public class HardwareData { private boolean data; public HardwareData(boolean data) { this.data = data; } // pour accéder a DATA public boolean get() { return data; } public void set(boolean data) { this.data = data; } //Suite…

  22. Simulation de l’équipement (mémoire et instructions atomiques) suite public boolean getAndSet(boolean data) { boolean oldValue = this.get(); this.set(data); return oldValue; } public void swap(HardwareData other) { boolean temp = this.get(); this.set(other.get()); other.set(temp); } } // fin de la définition de la classe

  23. Fil utilisant une commande “get-and-set” // le verrou est partagé par tous les fils HardwareData lock = new HardwareData(false); while (true) { while (lock.getAndSet(true)) Thread.yield(); // attend la libération du verrou sectioncritique(); lock.set(false); sectionNonCritique(); }

  24. Fil utilisant une instruction “swap” // le verrou est partagé par tous les fils HardwareData lock = new HardwareData(false); // chaque fil possède une copie locale de la clé (key) HardwareData key = new HardwareData(true); while (true) { // boucle infinie pour tester key.set(true); do { lock.swap(key); } while (key.get() == true); sectionCritique(); lock.set(false); sectionNonCritique(); }

  25. Les Sémaphores • C’est une technique courante pour la synchronisation des processus au niveau des programmes utilisateurs • Désavantage : le processus qui souhaite acquérir le sémaphore doit attendre si ce dernier est déjà pris par un autre processus : spinlock • Un sémaphore c’est une variable de type entier dont l’utilisation se réduit à 3 commandes atomiques : • Initialisation • Acquisition (obtenir) obtenir(S) { while S <= 0 ; // on ne fait rien, un autre processus a acquis le //sémaphore S--; // on déduit 1 (sémaphore pris) } • Libération (liberer) liberer(S) { S++; // on ajoute 1 (sémaphore libéré) }

  26. Les Sémaphores comme outils de synchronisation • Sémaphore compteur – une variable qui peut prendre n’importe quelle valeur entière • Sémaphore binaire – une variable qui peut prendre seulement les valeurs 0 et 1 • Pour réaliser l’exclusion nécessaire en cas de sections critiques, Semaphore S; // initialisation à 1 obtenir(S); sectioncritique(); liberer(S);

  27. Les semaphores (suite) • Le désavantage principal des algorithmes de synchronisation présentés antérieurement consiste dans le fait que lorsqu’un processus est dans la section critique, tout autre processus qui veut exécuter la section critique doit attendre en bouclant, ce qui constitue une utilisation improductive de l’UC. D’autres processus pourraient l’utiliser. • Les verrous qui agissent ainsi portent le nom de verrous tournants (spinlocks). Ils sont utiles lorsque les attentes prévues sont courtes ou dans le cas des multiprocesseurs car un fil peut s’exécuter sur un processeur différent.

  28. Les semaphores (suite) • On peut éviter les verrous tournants en utilisant une queue d’attente et le blocage du fil le temps de libérer le processeur occupé par la section critique d’un autre processus. Le fil bloqué passe en état d’attente et l’ordonnanceur donne le contrôle à un autre processus. • Le processus bloquant exécute une opération de libération du sémaphore lorsque la section critique s’est terminée et le processus bloqué est réveillé pour le passer dans la queue des processus prêts. • Pour fonctionner ainsi, le sémaphore est construit avec une variable entière et une liste de processus.

  29. Synchronisation à l’aide de sémaphore - usine public class usine implements Runnable{ private Semaphore sem; private String nom; public usine(Semaphore sem, String nom) { this.sem = sem; this.nom = nom; } public void run() { while (true) { sem.obtenir(); // en attente du sémaphore MutlExUtil.sectioncritique(nom); sem.liberer(); MutlExUtil.sectionNonCritique(nom); } } }

  30. Synchronisation à l’aide de sémaphore – le programme principal public class SimulateurSemaphore { public static void main(String args[]) { Semaphore sem = new Semaphore(1); Thread[ ] fils = new Thread[5]; for (int i = 0; i < 5; i++) fils[i] = new Thread(new usine(sem, "usine " + (new Integer(i)).toString() )); for (int i = 0; i < 5; i++) fils[i].start(); } }

  31. Implémentation du sémaphore obtenir(S){ value--; if (value < 0) { ajouter ce processus à la liste d’attente bloquer ; // au lieu de boucler, ce qui permet à l’Ordonnanceur d’en choisir un autre pour exécution. } } liberer(S){ value++; if (value <= 0) { enlever un processus de la liste d’attente réveiller(P); } }

  32. Implémentation du sémaphore Les opérations de blocage, mise en queue d’attente, réveil et remise dans la queue des processus prêts sont implémentées comme des primitives du SE. • Doit garantir l’impossibilité que deux processus soient capables d’exécuter obtenir() et liberer() sur le même sémaphore en même temps • L’implémentation est ainsi réduite à un problème de section critique, avec les conséquences décrites antérieurement (risques d’attentes prolongées mais facilité de réalisation)

  33. Blocages (Deadlock and Starvation) • L’utilisation des sémaphores et des queues d’attente peut donner lieu à 2 situations de blocage : • Interblocage : deux ou plusieurs processus attendent indéfiniment un évènement qu’un autre parmi eux peut seul produire (“etreinte mortelle”). • Exemple : Soit S et Q deux sémaphores initialisés à 1 P0 P1 obtenir(S); obtenir(Q); obtenir(Q); obtenir(S); . . liberer(S); liberer(Q); liberer(Q); liberer(S); • Attente infinie : le processus n’est jamais enlevé de la queue d’accès au sémaphore

  34. Problèmes classiques de synchronisation • Le tampon de taille finie (problème du producteur / consommateur) • Lecture et écriture dans une base de données • Le problème des philosophes

  35. Le tampon de taille finie public class BoundedBuffer implements Buffer { private static final int BUFFER SIZE = 5; // 5 objets produits/consommés private Object[] buffer; private int in, out; private Semaphore mutex; // assure l’exclusion mutuelle lors de l’accès au tampon private Semaphore empty; // compteur du nombre de tampons libres private Semaphore full; // compteur du nombre de tampons occupés // Suite

  36. Le tampon de taille finie (suite) public BoundedBuffer() { // le tampon est initialement vide in = 0; out = 0; buffer = new Object[BUFFER SIZE]; mutex = new Semaphore(1); empty = new Semaphore(BUFFER SIZE); full = new Semaphore(0); } public void insert(Object item) { /* voir suite */ } public Object remove() { /* voir suite */ } }

  37. Le tampon de taille finie (suite) public void insert(Object item) { empty.obtenir(); mutex.obtenir(); // ajouter un élément au tampon buffer[in] = item; in = (in + 1) % BUFFER SIZE; mutex.liberer(); full.liberer(); }

  38. Le tampon de taille finie (suite) public Object remove() { full.obtenir(); mutex.obtenir(); // enlève un élément du tampon Object item = buffer[out]; out = (out + 1) % BUFFER SIZE; mutex.liberer(); empty.liberer(); return item; }

  39. Le tampon de taille finie (suite) import java.util.Date; public class Producer implements Runnable { // le producteur private Buffer buffer; public Producer(Buffer buffer) { this.buffer = buffer; } public void run() { Date message; while (true) { // attendre… SleepUtilities.nap(); // produit un message et ajoute au tampon message = new Date(); buffer.insert(message); } } }

  40. Le tampon de taille finie (suite) import java.util.Date; public class Consumer implements Runnable { // le consommateur private Buffer buffer; public Consumer(Buffer buffer) { this.buffer = buffer; } public void run() { Date message; while (true) { // attendre… SleepUtilities.nap(); // enlève un élément du tampon message = (Date)buffer.remove(); } } }

  41. Le tampon de taille finie (suite) public class Factory { public static void main(String args[]) { Buffer buffer = new BoundedBuffer(); // création des fils consommateur et producteur Thread producer = new Thread(new Producer(buffer)); Thread consumer = new Thread(new Consumer(buffer)); producer.start(); consumer.start(); } }

  42. class Buffer { private static final MAX_AVAILABLE = 100; private final Semaphore available = new Semaphore(MAX_AVAILABLE, true); public Object getItem() throws InterruptedException { available.acquire(); // si disponibilité obtient le sémaphore, sinon bloque le thread return getNextAvailableItem(); } public void putItem(Object x) { if (markAsUnused(x)) available.release(); // libère le sémaphore } protected Object[] items = ... Les objets qu’il faut gérer de manière protégée boolean[] used = new boolean[MAX_AVAILABLE]; protected synchronized Object getNextAvailableItem() { for (int i = 0; i < MAX_AVAILABLE; ++i) { if (!used[i]) { used[i] = true; return items[i]; } } return null; // not reached } protected synchronized boolean markAsUnused(Object item) { for (int i = 0; i < MAX_AVAILABLE; ++i) { if (item == items[i]) { if (used[i]) { used[i] = false; return true; } else return false; } } return false; } }

  43. Lecture et écriture dans une base de données public class Reader implements Runnable { // la fonction “lecture” private RWLock db; public Reader(RWLock db) { this.db = db; } public void run() { while (true) { // attendre db.obtenirReadLock(); // Accès accordé // Lecture db.libererReadLock(); } } }

  44. Lecture et écriture dans une BD (suite) public class Writer implements Runnable { // la fonction “écriture” { private RWLock db; public Writer(RWLock db) { this.db = db; } public void run() { while (true) { db.obtenirWriteLock(); // Accès accordé // Ecrit db.libererWriteLock(); } } }

  45. Lecture et écriture dans une BD (suite) public interface RWLock { public abstract void obtenirReadLock(); public abstract void obtenirWriteLock(); public abstract void libererReadLock(); public abstract void libererWriteLock(); }

  46. Lecture et écriture dans une BD (suite)La base de données public class Database implements RWLock { private int readerCount; private Semaphore mutex; private Semaphore db; public Database() { readerCount = 0; mutex = new Semaphore(1); db = new Semaphore(1); } public int obtenirReadLock() { /* voir suite */ } public int libererReadLock() {/* voir suite */ } public void obtenirWriteLock() {/* voir suite */ } public void libererWriteLock() {/* voir suite */ } }

  47. Lecture et écriture dans une BD (suite)Méthodes utilisée pour la lecture public void obtenirReadLock() { mutex.obtenir(); ++readerCount; // le premier lecteur en cours de lecture if (readerCount == 1) db.obtenir(); mutex.liberer(); } public void libererReadLock() { mutex.obtenir(); --readerCount; // le dernier lecteur a terminé if (readerCount == 0) db.liberer(); mutex.liberer(); }

  48. Lecture et écriture dans une BD (suite)Méthodes utilisée pour l’écriture public void obtenirWriteLock() { db.obtenir(); } public void libererWriteLock() { db.liberer(); }

  49. Le problème des philosophes chinois

  50. Le problème des philosophes chinois - enoncé • Énoncé • 5 philosophes réfléchissent autour d’une table ronde • Chacun a devant lui un plat de riz • Entre chaque plat de riz est disposée une baguette • Pour manger, un philosophe doit utiliser 2 baguettes, mais ne peut utiliser que celles qui se trouvent autour de son plat • Baguettes = données partagées (stick = baguette). Le philosophe qui veut manger prend les baguettes dans l’ordre gauche -> droite • Seulement 2 philosophes peuvent manger en même temps • Les deux philosophes qui mangent ne peuvent pas se trouver l’un a coté de l’autre • Après avoir mangé, un philosophe pose les baguettes utilisées au même endroit et dans le même ordre. • Ce problème est représentatif d’une large classe de problèmes de synchronisation de processus

More Related