Chapitre 3 : Gérer la communication et la synchronisation inter-tâches 1
Plan du cours Introduction aux problèmes de synchronisation Exemple de problèmes Section critique et verrous Exclusion Mutuelle 2
Introduction Programme multi-tâches : coopération inter-tâches Echange ou partage d'informations Synchronisation pour le respect de la causalité et pour la protection des données Deux modèles Système centralisé : privilégie la communication et la synchronisation par mémoire commune Système distribué : privilégie la communication et la synchronisation par messages 3
Caractéristiques des systèmes distribués Ensemble de machines reliées en réseau (pouvant être temps réel) Pas de mémoire commune (et plusieurs ordonnanceurs) Coopération et synchronisation basées sur l échange de messages permet l échange de données (un message transporte des données) synchronisation implicite : on ne peut recevoir un message que s'il a été émis permet aussi une synchronisation explicite : le message peut transporter une information de synchronisation 4
Mécanismes présents dans les systèmes distribués : mécanismes de communication Appel de procédures distantes envoie d une demande à un serveur plusieurs sémantiques (appel bloquant ou non bloquant) mécanisme système exemple : RPC Invocation de méthodes distantes appel d une méthode d un objet situé sur un site distant mécanisme langage exemple : Java RMI (Remote Method Invocation) 5
Caractéristiques des systèmes centralisés Une seule machine pour plusieurs tâches : partage obligatoire Mémoire commune Communication et synchronisation par partage de données en mémoire simple à mettre en oeuvre pose le problème de la protection des données par rapport à des accès multiples (problème de cohérence) lorsque les opérations ne sont pas atomiques 6
Synchronisation dans les systèmes centralisés 7
Pourquoi des mécanismes de protection? Considérons l exemple suivant : Deux tâches partagent un registre Le registre est en fait un tableau de N entiers Les tâches doivent régulièrement incrémenter chaque case du registre L incrémentation doit se faire en N opérations élémentaires Nous étudions un programme implémentant cet exemple en C avec la norme Posix puis avec les langages Java et Ada 8
Mécanismes présents dans les systèmes centralisés : mécanismes de protection Masquage des interruptions dangereux, à réserver à des zones très ciblées (noyau système) Test and Set bas niveau, plutôt matériel (au niveau du processeur) Sémaphore (Dijkstra - 1965) assez bas niveau mais très commun et simple dans le cas général Moniteur haut-niveau, le plus commode et le plus sûr (au niveau langage) Mécanismes basés sur l'échange de données également possible 9
Exemple incorrect (Java) : primitive E/S class Registre { int[] Tab; public Registre(int n) { Tab = new int[n]; public int[] lit_registre() { int[] res = new int[this.tab.length]; for (int i = 0; i < res.length; i++) { res[i] = this.tab[i]; return res; public void ecrit_registre(int[] T) { for (int i = 0; i < Tab.length; i++) { Tab[i] = T[i]; 10
Exemple incorrect (Java) : code des tâches Public class monthread extends Thread { Registre leregistre; monthread(registre R) { leregistre = R; public void run() { for (int turn=0; turn<1000000; turn++) { int Tab[]; Tab = leregistre.lit_registre(); for (int i = 0; i < Tab.length; i++) { Tab[i]++; leregistre.ecrit_registre(tab); 11
Exemple incorrect (Java) : le principal public class Prog1 { public static void main(string args[]) throws InterruptedException{ Registre R = new Registre(10); monthread Th1 = new monthread(r); monthread Th2 = new monthread(r); Th1.start(); // démarrage des threads Th2.start(); Th1.join(); // attente de la terminaison des threads Th2.join(); int[] T = R.lit_Registre(); System.out.println(»Valeur finale du Registre»); for (int i = 0; i<t.length; i++){ System.out.println(T[i]); 12
Exemple incorrect : exécution du programme Voici un exemple d'exécution (C/Posix, Java) Valeur finale du registre 1543280 1543280 1177867 1177867 1177867 1177867 1177867 1177867 1177867 1177867 On aurait du avoir 10 lignes avec la valeur 2000000 (deux millions) il est nécessaire de garantir qu une séquence lecture / incrémentation / écriture soit finie avant qu une autre ne puisse commencer! 13
Opération atomique Une opération atomique c'est: Une opération ou un ensemble d opérations d'un programme pouvant être combinées afin qu elles apparaissent comme une unique opération pour le système. De ce fait, ces opérations s'exécutent entièrement sans pouvoir être interrompue avant la fin de leur déroulement. Ceci permet que une donnée reste thread safe (plusieurs thread ne peuvent s'interlacer durant une modification). 14
Mécanismes de synchronisation Deux mécanismes classiques peuvent être utilisés pour protéger des données accédées par plusieurs tâches : Les sémaphores ne peuvent être pris qu un nombre déterminé de fois (si le sémaphore est déjà pris une nouvelle tentative bloquera l appelant) Les moniteurs qui permettent «d encapsuler» des données en définissant des règles d accès exclusif à ces données D'un point de vue bas étage: Les sémaphores qui ne peuvent être pris qu un nombre déterminé de fois (si le sémaphore est déjà pris une nouvelle tentative bloquera l appelant) Les moniteurs permettent «d encapsuler» des données en définissant des règles d accès exclusif à ces données 15
Sections critiques en Java Chaque objet possède un verrou récursif (lock) et une file d attente associée à ce verrou Ce verrou ne peut, à un moment donné, être possédé que par un seul thread Ce thread peut le posséder en plusieurs exemplaires Ce verrou peut être possédé par différentes thread à différents moments Ce verrou est pris lorsque l on accède à une méthode (ou à un bloc) marqué par le mot clef «synchronized» Ce verrou est rendu (entre autres) lorsque la thread qui le détenait termine la méthode ou le bloc «synchronized» Lorsque le verrou est pris et qu un thread tente de le prendre, le thread est mis en attente dans la file correspondante; il sera débloqué automatiquement lorsque le verrou sera relâché
Sections critiques en Java (suite) Chaque bloc ou méthode «synchronized» définit donc une section critique au niveau de l objet Prise du verrou au niveau de l objet o Synchronized(o){ Code de la section critique ; Suite du code ; Relâche du verrou Synchronized(o){ Code de la section critique ; Suite du code ;
Sections critiques en Java (suite) Lorsque les méthodes sont marquées «synchronized» elles font référence implicitement à «this» ; elles sont alors à accès exclusif. Attention : l accès exclusif ne concerne que les méthodes marquées «synchronized» du même objet Class C{ public synchronized typem1 methode1{ Code de la méthode m1 ; Suite du code ; est equivalent à public void typem1 methode1{ synchronized(this);{
Sections critiques en Java (suite) Un petit schéma explicatif?? On suppose un objet A partagé entre deux threads T1 et T2 (qui font appel à la même instance de l'objet). On suppose qu'il y a une méthode A.blabla() qui est synchronisée. Comment cela s'écrit-il? Représentez le déroulement de l'exécution Mécanisme à illustrer verrou, code, etc...
Exemple incorrect (Java) : primitive E/S class Registre { int[] Tab; public Registre(int n) { Tab = new int[n]; public int[] lit_registre() { int[] res = new int[this.tab.length]; for (int i = 0; i < res.length; i++) { res[i] = this.tab[i]; return res; public void ecrit_registre(int[] T) { for (int i = 0; i < Tab.length; i++) { Tab[i] = T[i]; 20
Exemple incorrect (Java) : Rappel code des taches class monthread extends Thread { Registre leregistre; monthread(registre R) { leregistre = R; public void run() { for (int turn=0; turn<1000000; turn++) { int Tab[]; Tab = leregistre.lit_registre(); for (int i = 0; i < Tab.length; i++) { Tab[i]++; leregistre.ecrit_registre(tab); 21
Sections critiques en Java (exemple incorrect avec synchronized incorrect) class Registre { int[] Tab; public Registre(int n) { Tab = new int[n]; Le code de ces méthodes est en section critique, mais cela Ne règle pas le problème précédent public synchronized int[] lit_registre() { int[] res = new int[this.tab.length]; for (int i = 0; i < res.length; i++) { res[i] = Tab[i]; return res; public synchronized void ecritregistre(int[] T) { for (int i = 0; i < Tab.length; i++) { Tab[i] = T[i];
Sections critiques en Java (correction de l exemple incorrect) class monthread extends Thread { Registre leregistre; monthread(registre R) { leregistre = R; Cette fois ci la section critique est Bien placée : pas de rupture de séquence Entre la lecture et l écriture du registre public void run() { for (int turn=0; turn<1000000; turn++) { int Tab[]; synchronized(leregistre){ Tab = leregistre.litregistre(); for (int i = 0; i < Tab.length; i++) { Tab[i]++; leregistre.ecritregistre(tab);
exécution du programme après correction Voici un exemple d'exécution La case 0 du registre vaut 2000000 La case 1 du registre vaut 2000000 La case 2 du registre vaut 2000000 La case 3 du registre vaut 2000000 La case 4 du registre vaut 2000000 La case 5 du registre vaut 2000000 La case 6 du registre vaut 2000000 La case 7 du registre vaut 2000000 La case 8 du registre vaut 2000000 La case 9 du registre vaut 2000000 On a bien 10 lignes avec la valeur 2000000 (deux millions) il est nécessaire de garantir qu une séquence lecture / incrémentation / écriture soit finie avant qu une autre ne puisse commencer -> problème corrigé!