Αμοιβαίος αποκλεισμός με κοινή μνήμη 1 lalis@inf.uth.gr
Το πρόβλημα Έστω ότι δύο η περισσότερα νήματα επιθυμούν να προσπελάσουν έναν κοινό πόρο, που όμως δεν μπορεί να χρησιμοποιηθεί ταυτόχρονα Η χρήση του πόρου πρέπει να γίνει με κατάλληλο συντονισμό ανάμεσα στα νήματα Η πλέον κλασική περίπτωση: ταυτόχρονη πρόσβαση νημάτων σε κοινές μεταβλητές (όπου μπορεί να προκύψουν ανεπιθύμητες συνθήκες ανταγωνισμού) 2 lalis@inf.uth.gr
3 lalis@inf.uth.gr
Παράδειγμα: χρήση κοινής μεταβλητής int i=0; P1: i=i+1 P2: i=i+1 tmp1=i i=tmp1+1 tmp2=i i=tmp2+1 Οι εντολές του ενός νήματος πιθανώς να μην εκτελεστούν ατομικά, χωρίς να μεσολαβήσει ανταγωνιστική εντολή του άλλου νήματος Μπορεί να προκύψει «ασυνέπεια» ως προς την τιμή που θα λάβει η κοινή μεταβλητή i 4 lalis@inf.uth.gr
Προσέγγιση Α Έχουμε συνειδητοποιήσει το πρόβλημα Γράφουμε κώδικα που επιτυγχάνει το επιθυμητό αποτέλεσμα ανεξάρτητα από την μη ατομικότητα των εντολών και τις εναλλαγές που ίσως γίνουν κατά την διάρκεια της εκτέλεσης του κώδικα Όμως Ο προγραμματιστής πρέπει να σκέφτεται / λύνει παρόμοια προβλήματα, κάθε φορά από την αρχή Υπάρχουν προβλήματα συγχρονισμού ανάμεσα σε νήματα που δεν μπορούν να λυθούν με απλό τρόπο 5 lalis@inf.uth.gr
Προσέγγιση Β Προσπαθούμε να μοντελοποιήσουμε το πρόβλημα και την λύση του με γενικό τρόπο, έτσι ώστε να μπορεί να χρησιμοποιηθεί σε πολλές περιπτώσεις Θετικά Η λύση είναι επαναχρησιμοποιήσιμη Διευκολύνεται η κατασκευή ορθών προγραμμάτων Αρνητικά Η γενική λύση ίσως επιφέρει επιπλέον κόστος εκτέλεσης (overhead) 6 lalis@inf.uth.gr
Κρίσιμο τμήμα Έστω ότι ο κώδικας ενός νήματος περιέχει μια ακολουθία εντολών που πρέπει εκτελεστούν χωρίς την παρεμβολή ανταγωνιστικού κώδικα προσοχή: όχι απαραίτητα ατομικά, αρκεί να μην παρεμβληθούν κάποιες ανταγωνιστικές εντολές Ένα τέτοιο τμήμα κώδικα ονομάζεται κρίσιμο τμήμα (critical section) Κάθε νήμα μπορεί να έχει πολλά κρίσιμα τμήματα, σε σχέση με ένα ή περισσότερα άλλα νήματα 7 lalis@inf.uth.gr
P Q R p1; p2; p3; p4; p5; p6; p7; p8; p9; q1; q2; q3; q4; q5; q6; q7; q8; q9; r1; r2; r3; r4; r5; r6; r7; r8; r9; 8 lalis@inf.uth.gr
P Q R p1; p2; p3; p4; p5; p6; p7; p8; p9; q1; q2; q3; q4; q5; q6; q7; q8; q9; r1; r2; r3; r4; r5; r6; r7; r8; r9; 9 lalis@inf.uth.gr
P Q R p1; p2; p3; p4; p5; p6; p7; p8; p9; q1; q2; q3; q4; q5; q6; q7; q8; q9; r1; r2; r3; r4; r5; r6; r7; r8; r9; 10 lalis@inf.uth.gr
Σημείωση Ο μεταγλωττιστής, το περιβάλλον ταυτόχρονης εκτέλεσης και το λειτουργικό δεν γνωρίζουν την σημασία των δεδομένων και του κώδικα του προγράμματος σε επίπεδο εφαρμογής Ο ίδιος ο προγραμματιστής είναι υπεύθυνος να εντοπίσει στον κώδικα του τα τμήματα που είναι όντως κρίσιμα ανάλογα με την επιθυμητή λειτουργικότητα του προγράμματος 11 lalis@inf.uth.gr
int i=0; P1: int k; for (k=0; k<n; k++) { i=i+1; } P2: int k; for (k=0; k<n; k++) { i=i+1; } Επιθυμητή λειτουργικότητα: στο τέλος της εκτέλεσης η i πρέπει να έχει την τιμή 2Ν οι εντολές i=i+1 αποτελούν κρίσιμο τμήμα 12 lalis@inf.uth.gr
int i=0; P1: while (i<2*n) { i=i+1; } P2: while (i<2*n) { i=i+1; } Επιθυμητή λειτουργικότητα: στο τέλος της εκτέλεσης η i πρέπει να έχει την τιμή >=2Ν οι εντολές i=i+1 δεν αποτελούν κρίσιμο τμήμα 13 lalis@inf.uth.gr
Ιδιότητες της λύσης του ΚΤ 1. Αμοιβαίος αποκλεισμός (mutual exclusion): αν ένα νήμα βρίσκεται μέσα στο ΚΤ του, τότε κανένα άλλο νήμα δεν θα εισέλθει στο δικό του ΚΤ 2. Πρόοδος (progress): αν ένα νήμα επιθυμεί να εισέλθει στο ΚΤ του, τελικά θα το καταφέρει Απουσία αδιεξόδου (no deadlock): δεν υπάρχει περίπτωση τα νήματα να εμποδίζουν το ένα το άλλο από το να εισέλθει στο ΚΤ έτσι ώστε κανένα νήμα να μην καταφέρει ποτέ να εισέλθει στο ΚΤ Απουσία λιμοκτονίας (no starvation): δεν υπάρχει περίπτωση ένα συγκεκριμένο νήμα να μην καταφέρει ποτέ να μπει στο ΚΤ ενώ άλλα νήματα το καταφέρνουν ή δεν ενδιαφέρονται να μπουν στο ΚΤ 14 lalis@inf.uth.gr
Κώδικας συγχρονισμού Οι λύσεις που θα εξετάσουμε αποτελούνται από δύο τμήματα κώδικα ανά κρίσιμο τμήμα Κώδικας εισόδου: εντολές πριν το ΚΤ, με κύριο σκοπό τον αμοιβαίο αποκλεισμό Κώδικας εξόδου: εντολές μετά το ΚΤ, με κύριο σκοπό την πρόοδο των νημάτων που τυχόν περιμένουν (στον κώδικα εισόδου) Ο κώδικας εισόδου και εξόδου σχεδιάζονται σε στενό συνδυασμό μεταξύ τους Οι λύσεις είναι συχνά συμμετρικές: όλα τα νήματα εκτελούν (σχεδόν) τον ίδιο κώδικα 15 lalis@inf.uth.gr
P0 P1 EntryCode0 EntryCode1 ΚΤ του P0 CS μας ενδιαφέρει αυτός ο κώδικας CS ΚΤ του P1 ExitCode0 ExitCode1 16 lalis@inf.uth.gr
P1 P2 P3 P1 P2 P3 1) 2) CS CS P1 P2 P3 P2 P3 3) CS 4) P1 CS P1 17 lalis@inf.uth.gr
Σημείωση Ο κώδικας εισόδου και ο κώδικας εξόδου αποτελείται (και αυτός) από συμβατικές εντολές Τα πρόβλημα της μη ατομικότητας των εντολών εξακολουθεί να υφίσταται και για τον κώδικα εισόδου/εξόδου όπως και για το κρίσιμο τμήμα Ο κώδικας εισόδου και εξόδου γράφεται με πλήρη επίγνωση της μη ατομικότητας, και έτσι ώστε να ικανοποιούνται οι ιδιότητες του ΚΤ Αυτό δεν είναι τόσο απλό 18 lalis@inf.uth.gr
Υποθέσεις εργασίας Ένα νήμα που είναι έτοιμο προς εκτέλεση, τελικά (κάποια στιγμή) θα λάβει τον επεξεργαστή δεν υπάρχει λιμοκτονία σε επίπεδο χρονοπρογραμματισμού Δεν γίνεται καμία υπόθεση για το αν/πότε γίνεται εναλλαγή ανάμεσα στα νήματα, και την σειρά με την οποία θα τους δοθεί ο επεξεργαστής Δεν γίνεται καμία υπόθεση για το πόσες φορές ένα νήμα θα θελήσει να εισέλθει στο ΚΤ Ένα νήμα δεν τερματίζεται κατά την εκτέλεση του κώδικα εισόδου/εξόδου ούτε μέσα στο ΚΤ Ένα νήμα δεν μένει για πάντα μέσα στο ΚΤ Για αρχή, υποθέτουμε μόνο 2 νήματα 19 lalis@inf.uth.gr
Αλγόριθμος Α1 int turn=p; P p1: p2: while (turn!=p) nop; p3: CS p4: turn=q; p5: Q q1: q2: while (turn!=q) nop; q3: CS q4: turn=p; q5: Ένα νήμα εισέρχεται στο ΚΤ όταν είναι η σειρά του Όταν βγει από το ΚΤ, δίνει σειρά στο άλλο νήμα 20 lalis@inf.uth.gr
Αλγόριθμος Α1 int turn=p; P p1: p2: while (turn!=p) nop; p3: CS p4: turn=q; p5: Q q1: q2: while (turn!=q) nop; q3: CS q4: turn=p; q5: Ένα νήμα εισέρχεται στο ΚΤ όταν είναι η σειρά του Όταν βγει από το ΚΤ, δίνει σειρά στο άλλο νήμα Εγγυάται αμοιβαίο αποκλεισμό γιατί; Εγγυάται απουσία αδιεξόδου γιατί; Δεν εγγυάται απουσία λιμοκτονίας γιατί; 21 lalis@inf.uth.gr
turn=p p1: p2: while (turn!=p) nop /* turn is P */ p3: CS q1: q1: p3: CS p4: turn=q p5: q1: q1: p2: while (turn!=p) nop /* turn is Q */ p2: while (turn!=p) nop /* turn is Q */ p2: while (turn!=p) nop /* turn is Q */ q1: q1: 22 lalis@inf.uth.gr
Αλγόριθμος A2 int turn; P p1: p2: turn=p; p3: while (turn!=p) nop; p4: CS p5: turn=q; p6: Q q1: q2: turn=q; q3: while (turn!=q) nop; q4: CS q5: turn=p; q6: Όπως προηγουμένως, αλλά τώρα κάθε νήμα δίνει προκαταβολικά σειρά στον εαυτό του 23 lalis@inf.uth.gr
Αλγόριθμος A2 int turn; P p1: p2: turn=p; p3: while (turn!=p) nop; p4: CS p5: turn=q; p6: Q q1: q2: turn=q; q3: while (turn!=q) nop; q4: CS q5: turn=p; q6: Όπως προηγουμένως, αλλά τώρα κάθε νήμα δίνει προκαταβολικά σειρά στον εαυτό του Δεν εγγυάται αμοιβαίο αποκλεισμό γιατί; 24 lalis@inf.uth.gr
p1: p2: turn=p p3: while (turn!=p) nop /* turn is P */ p4: CS p4: CS p4: CS q1: q1: q2: turn=q q3: while (turn!=q) nop /* turn is Q */ q4: CS q4: CS 25 lalis@inf.uth.gr
Αλγόριθμος Β1 boolean wantp=false,wantq=false; P p1: p2: while (wantq) nop; p3: wantp=true; p4: CS p5: wantp=false; p6: Q q1: q2: while (wantp) nop; q3: wantq=true; q4: CS q5: wantq=false; q6: Χρησιμοποιούμε ξεχωριστές μεταβλητές για την πρόθεση εισόδου κάθε νήματος στο ΚΤ Προτού ένα νήμα εισέλθει στο ΚΤ, ελέγχει την πρόθεση του άλλου, και αν ισχύει τότε περιμένει 26 lalis@inf.uth.gr
Αλγόριθμος Β1 boolean wantp=false,wantq=false; P p1: p2: while (wantq) nop; p3: wantp=true; p4: CS p5: wantp=false; p6: Q q1: q2: while (wantp) nop; q3: wantq=true; q4: CS q5: wantq=false; q6: Χρησιμοποιούμε ξεχωριστές μεταβλητές για την πρόθεση εισόδου κάθε νήματος στο ΚΤ Προτού ένα νήμα εισέλθει στο ΚΤ, ελέγχει την πρόθεση του άλλου, και αν ισχύει τότε περιμένει Δεν εγγυάται αμοιβαίο αποκλεισμό γιατί; 27 lalis@inf.uth.gr
wantp=false, wantq=false p1: p1: p2: while (wantq) nop /* wantq is false */ q1: q1: q2: while (wantp) nop /* wantp is false */ q3: wantq=true q4: CS q4: CS q4: CS p3: wantp=true p4: CS p4: CS p4: CS 28 lalis@inf.uth.gr
Αλγόριθμος B2 boolean wantp=false,wantq=false; P p1: p2: wantp=true; p3: while (wantq) nop; p4: CS p5: wantp=false; p6: Q q1: q2: wantq=true; q3: while (wantp) nop; q4: CS q5: wantq=false; q6: Όπως προηγουμένως, αλλά τώρα κάθε νήμα δηλώνει τον πρόθεση του να εισέλθει στο ΚΤ προτού ελέγξει την πρόθεση του άλλου νήματος 29 lalis@inf.uth.gr
Αλγόριθμος B2 boolean wantp=false,wantq=false; P p1: p2: wantp=true; p3: while (wantq) nop; p4: CS p5: wantp=false; p6: Q q1: q2: wantq=true; q3: while (wantp) nop; q4: CS q5: wantq=false; q6: Όπως προηγουμένως, αλλά τώρα κάθε νήμα δηλώνει τον πρόθεση του να εισέλθει στο ΚΤ προτού ελέγξει την πρόθεση του άλλου νήματος Εγγυάται αμοιβαίο αποκλεισμό γιατί; Δεν εγγυάται απουσία αδιεξόδου γιατί; 30 lalis@inf.uth.gr
wantp=false, wantq=false p1: p1: p2: wantp=true q1: q1: q2: wantq=true q3: while (wantp) nop /* wantp is true */ q3: while (wantp) nop /* wantp is true */ q3: while (wantp) nop /* wantp is true */ p3: while (wantq) nop /* wantq is true */ p3: while (wantq) nop /* wantq is true */ p3: while (wantq) nop /* wantq is true */ q3: while (wantp) nop /* wantp is true */ q3: while (wantp) nop /* wantp is true */ q3: while (wantp) nop /* wantp is true */ p3: while (wantq) nop /* wantq is true */ p3: while (wantq) nop /* wantq is true */ p3: while (wantq) nop /* wantq is true */ 31 lalis@inf.uth.gr
Αλγόριθμος B3 boolean wantp=false,wantq=false; P Q p1: p2: wantp=true; p3: while (wantq) { p4: wantp=false; p5: wantp=true; } p6: CS p7: wantp=false; p8: q1: q2: wantq=true; q3: while (wantp) { q4: wantq=false; q5: wantq=true; } q6: CS q7: wantq=false; q8: Όπως προηγουμένως, αλλά κατά την αναμονή γίνεται προσωρινή «παραίτηση» από την πρόθεση εισόδου 32 lalis@inf.uth.gr
Αλγόριθμος B3 boolean wantp=false,wantq=false; P p1: p2: wantp=true; p3: while (wantq) { p4: wantp=false; p5: wantp=true; } p6: CS p7: wantp=false; p8: Q q1: q2: wantq=true; q3: while (wantp) { q4: wantq=false; q5: wantq=true; } q6: CS q7: wantq=false; q8: Όπως προηγουμένως, αλλά κατά την αναμονή γίνεται προσωρινή «παραίτηση» από την πρόθεση εισόδου Εγγυάται αμοιβαίο αποκλεισμό γιατί; Δεν εγγυάται πρόοδο γιατί; 33 lalis@inf.uth.gr
wantp=false, wantq=false p1: p2: wantp=true q1: q2: wantq=true q3: while (wantp) /* wantp is true */ q4: wantq=false q5: wantq=true p3: while (wantq) {} /* wantq is true */ p4: wantp=false p5: wantp=true p3: while (wantq) {} /* wantq is true */ q3: while (wantp) {} /* wantp is true */ q4: wantq=false q5: wantq=true p4: wantp=false p5: wantp=true p3: while (wantq) {} /* wantq is true */ 34 lalis@inf.uth.gr
Παρατηρήσεις H λύση είναι τυπικά λανθασμένη Παρόλα αυτά, μια τέτοια «ατυχής» εναλλαγή θα μπορούσε να θεωρηθεί εντελώς απίθανη στην πράξη η λύση θα μπορούσε να χαρακτηριστεί ως ικανοποιητική (τουλάχιστον για μη-κρίσιμα συστήματα) Όμως, αυτή η υπόθεση απαιτεί προσοχή Π.χ. αν γίνεται συστηματική εναλλαγή κάθε εντολή ή κάθε δεύτερη εντολή, υπάρχει σίγουρη λιμοκτονία Μπορεί να μειωθεί (από τον προγραμματιστή) η πιθανότητα μιας «ατυχούς» εκτέλεσης; 35 lalis@inf.uth.gr
Αλγόριθμος B4 boolean wantp=false,wantq=false; P p1: p2: wantp=true; p3: while (wantq) { p4: wantp=false; p5: sleep(random); p6: wantp=true; } p7: CS p8: wantp=false; p9: Q q1: q2: wantq=true; q3: while (wantp) { q4: wantq=false; q5: sleep(random); q6: wantq=true; } q7: CS q8: wantq=false; q9: Υπάρχει εγγύηση προόδου; 36 lalis@inf.uth.gr
Αλγόριθμος B5 boolean wantp=false,wantq=false; P p1: p2: wantp=true; p3: while (wantq) { p4: wantp=false; p5: while (wantq) nop; p6: wantp=true; } p7: CS p8: wantp=false; p9: Q q1: q2: wantq=true; q3: while (wantp) { q4: wantq=false; q5: nop; q6: wantq=true; } q7: CS q8: wantq=false; q9: Υπάρχει εγγύηση προόδου; 37 lalis@inf.uth.gr
Αλγόριθμος B6 boolean wantp=false,wantq=false; P p1: p2: wantp=true; p3: while (wantq) { p4: wantp=false; p5: while (wantq) nop; p6: wantp=true; } p7: CS p8: wantp=false; p9: Q q1: q2: wantq=true; q3: while (wantp) { } q4: CS q5: wantq=false; q6: Υπάρχει εγγύηση προόδου; 38 lalis@inf.uth.gr
Αλγόριθμος Dekker int turn=p; boolean wantp=false,wantq=false; P p1: p2: wantp=true; p3: while (wantq) { p4: if (turn!=p) { p5: wantp=false; p6: while (turn!=p) nop; p7: wantp=true; } } p8: CS p9: turn=q; p10: wantp=false; p11: Q q1: q2: wantq=true; q3: while (wantp) { q4: if (turn!=q) { q5: wantq=false; q6: while (turn!=q) nop; q7: wantq=true; } } q8: CS q9: turn=p; q10: wantq=false; q11: 39 lalis@inf.uth.gr
Παρατήρηση Ο αλγόριθμος συνδυάζει τους Α1 και B3 Όπως στον Β3, η πρόθεση εισόδου στο ΚΤ καταγράφεται μέσω των wantp/q Όπως στον Α1, η turn δίνει την προτεραιότητα σε περίπτωση ανταγωνισμού μεταξύ των νημάτων, ώστε να αποφεύγεται το αδιέξοδο Δεν μπορεί να γίνονται προσπεράσματα (για πάντα) 40 lalis@inf.uth.gr
Σενάριο προσπεράσματος Ο αλγόριθμος δεν αποκλείει πλήρως το προσπέρασμα Η είσοδος στο ΚΤ υπό συνθήκες ανταγωνισμού των δύο νημάτων, δεν είναι εγγυημένα εναλλάξ Παρότι είναι σειρά του P να μπει στο ΚΤ, αν δεν λάβει τον επεξεργαστή, μπορεί να προσπεραστεί από το Q, το οποίο να μπει για πολλοστή φορά στο ΚΤ Όμως, υποθέτουμε ότι τα νήματα λαμβάνουν τακτικά τον επεξεργαστή Από την στιγμή που το P λάβει τον έλεγχο και επαναφέρει την πρόθεση του για είσοδο στο ΚΤ, θα τηρηθεί η σειρά προτεραιότητας και θα μπει σίγουρα στο ΚΤ, χωρίς να προσπεραστεί από το Q 41 lalis@inf.uth.gr
42 lalis@inf.uth.gr
Άτυπη απόδειξη ορθότητας Λόγω συμμετρίας, αν αποδείξουμε τις παραπάνω ιδιότητες για P, τις έχουμε αποδείξει και για Q 1. Αμοιβαίος αποκλεισμός: Αν «P είναι στο ΚΤ» τότε «Q δεν είναι στο ΚΤ» 2. Πρόοδος: Αν «P επιθυμεί να μπει στο ΚΤ» τότε τελικά «P είναι στο ΚΤ» (άσχετα με το τι θα προσπαθήσει να κάνει το Q) 43 lalis@inf.uth.gr
Αμοιβαίος αποκλεισμός Α) Q επιχειρεί να μπει στο ΚΤ, ενώ P είναι ήδη στο KT wantp είναι true Q θα περιμένει στον εξωτερικό ή/και εσωτερικό βρόγχο αναμονής, ανάλογα με την τιμή της turn Β) P και Q επιχειρούν να μπουν στο ΚΤ ταυτόχρονα, και είναι σειρά του P να μπει στο ΚΤ (turn είναι P) wantp και wantq γίνονται true, προτού γίνει έλεγχος της συνθήκης αναμονής από P ή Q P και Q θα μπουν στον εξωτερικό βρόγχο αναμονής Q θα θέσει wantq σε false, και θα περιμένει στον εσωτερικό βρόγχο P θα βγει από τον εξωτερικό βρόγχο και θα μπει στο ΚΤ η τιμή της turn δεν αλλάζει (μένει P) όσο το P είναι στο ΚΤ Q θα περιμένει στον εσωτερικό βρόγχο 44 lalis@inf.uth.gr
Πρόοδος Α) Μόνο το P θέλει να μπει στο ΚΤ, ενώ Q όχι wantq είναι false, P θα εισέλθει άμεσα στο ΚΤ Β1) P και Q θέλουν να μπουν στο ΚΤ, και turn είναι P βλέπε σενάριο ΑΑ.Β Β2) P και Q θέλουν να μπουν στο ΚΤ, και turn είναι Q P θα περιμένει, στον εξωτερικό ή/και εσωτερικό βρόγχο Q θα βγει από ΚΤ, wantq θα γίνει false, turn θα γίνει P αν P είναι στον εσωτερικό βρόγχο κάποια στιγμή θα βγει από αυτόν και θα θέσει wantp σε true αν Q δεν επιχειρήσει να ξαναμπεί στο ΚΤ, η wantq θα μείνει false, οπότε P θα βγει (και) από τον εξωτερικό βρόγχο και θα μπει στο ΚΤ αν Q επιχειρήσει να μπει ξανά στο ΚΤ, βλέπε Β1 45 lalis@inf.uth.gr
46 lalis@inf.uth.gr
Ένας ακόμα αλγόριθμος Συνδυασμός ελέγχου των δύο συνθηκών εισόδου που υπάρχουν ήδη στον αλγόριθμο του Dekker 1) Μήπως επιχειρεί και το άλλο νήμα να μπει στο ΚΤ αυτή τη στιγμή; 2) Μήπως είναι η σειρά μου να μπω στο ΚΤ; Μόνο ένας βρόγχος αναμονής, μέσω του οποίου πραγματοποιούνται και οι δύο παραπάνω έλεγχοι 47 lalis@inf.uth.gr
Αλγόριθμος Peterson int turn; int wantp=false,wantq=false; P wantp=true; turn=q; while (wantq) { if (turn==p) { break; } } Q wantq=true; turn=p; while (wantp) { if (turn==q) { break; } } CS CS wantp=false; wantq=false; 48 lalis@inf.uth.gr
Αλγόριθμος Peterson int turn; int wantp=false,wantq=false; P wantp=true; turn=q; while ((wantq) && (turn!=p)) {} Q wantq=true; turn=p; while ((wantp) && (turn!=q)) {} CS CS wantp=false; wantq=false; 49 lalis@inf.uth.gr
Παρατήρηση Όπως και στον A2, και τα δύο νήματα αλλάζουν και ελέγχουν την turn ταυτόχρονα μεταξύ τους Αντίθετα με τον A2, σε αυτή την περίπτωση είναι εγγυημένος ο αμοιβαίος αποκλεισμός γιατί όταν ένα νήμα αλλάζει την turn τότε δίνει προτεραιότητα στο άλλο (και όχι στον εαυτό του) Ο αλγόριθμος δεν θα ήταν σωστός αν κάθε νήμα που επιχειρεί να εισέλθει στο ΚΤ έδινε προτεραιότητα στον εαυτό του (Α2) η αλλαγή της turn (υπέρ του άλλου νήματος) γινόταν μετά την έξοδο από το ΚΤ (Α1) 50 lalis@inf.uth.gr
Αλγόριθμος Bakery (Lamport) Σε περίπτωση ανταγωνισμού, η προτεραιότητα καθορίζεται μέσω ενός εισιτηρίου (ticket) t[i] Αν το Pi δεν έχει πρόθεση να μπει στο ΚΤ, t[i]==0 Διαφορετικά, το Pi λαμβάνει το εισιτήριο του t[i] αυξάνοντας κατά 1 το εισιτήριο t[(i+1)%2] του ανταγωνιστικού νήματος P(i+1)%2 Το νήμα με τον μικρότερο αριθμό εισέρχεται στο ΚΤ Δεν υπάρχει περίπτωση ένα νήμα να εισέλθει δύο συνεχόμενες φορές στο ΚΤ ενώ περιμένει το άλλο Στην δεύτερη απόπειρα θα λάβει σίγουρα μεγαλύτερο αριθμό εισιτηρίου από το νήμα που ήδη περιμένει να μπει στο ΚΤ 51 lalis@inf.uth.gr
Σύγκριση εισιτηρίων / προτεραιότητας bool priority(i) { if (t[i]==0) // Pi not competing => I have priority return(false); else if (t[self]<t[i]) // I have priority return(false); else if (t[self]>t[i]) // Pi has priority return(true); else // same ticket nr => tie-break based on ids return(self>i); } Λόγω ταυτόχρονης εκτέλεσης, διαφορετικά νήματα μπορεί να έχουν τον ίδιο αριθμό εισιτηρίου > 0 Τότε, προτεραιότητα εισόδου στο ΚΤ δίνεται (αυθαίρετα) στο νήμα με το μικρότερο αναγνωριστικό 52 lalis@inf.uth.gr
Αλγόριθμος Bakery / FIFO (Lamport) int t[2]={0,0}; bool taking[2]={false,false}; P0: taking[0]=true; t[0] = t[1] + 1; taking[0]=false; while (taking[1]) {} while (priority(1)) {} CS0 t[0]=0; P1 taking[1]=true; t[1] = t[0] + 1; taking[1]=false; while (taking[0]) {} while (priority(0)) {} CS1 t[1]=0; 53 lalis@inf.uth.gr
Η μεταβλητή taking Αν taking[i]==true, το εισιτήριο του Pi βρίσκεται υπό κατασκευή, και δεν μπορεί να χρησιμοποιηθεί για τον έλεγχο προτεραιότητας Αν taking[i]==false, το εισιτήριο του Pi μπορεί να χρησιμοποιηθεί με ασφάλεια για τον έλεγχο προτεραιότητας Αν, αφού ένα νήμα ελέγξει ότι taking[i]==false, αλλά προτού ελέγξει την προτεραιότητα, το Pi βγάλει εισιτήριο, αυτό θα είναι εγγυημένα μεγαλύτερο από το εισιτήριο του άλλου νήματος 54 lalis@inf.uth.gr
Αλγόριθμος Bakery / FIFO (Lamport) int t[2]={0,0}; int taking[2]={false,false}; P0: taking[0]=true; tmp0 = t[1]; t[0] = tmp0 + 1; taking[0]=false; while (taking[1]) {} while (priority(1)) {} CS0 t[0]=0; P1: taking[1]=true; tmp1 = t[0]; t[1] = tmp1 + 1; taking[1]=false; while (taking[0]) {} while (priority(0)){} CS1 t[1]=0; 55 lalis@inf.uth.gr
Χωρίς την «προστασία» της taking t[0]=0, t[1]=0 P0: tmp0=t[1] /* tmp0 is 0 */ P1: tmp1=t[0] /* tmp1 is 0 */ P1: t[1]=tmp1+1 /* t[1] is 1 */ P1: while (priority(0)) /* false */ P1: CS P0: t[0]=tmp0+1 /* t[0] is 1 */ P0: while(priority(1)) /* false */ P0: CS 56 lalis@inf.uth.gr
Αλγόριθμος Bakery για Ν νήματα int t[ν]={0,,0}; int taking[ν]={false,,false}; Pi taking[i]=true; t[i] = max(t[0],t[1],, t[n-1]) + 1; taking[i]=false; for (j=0; j<ν; j++) { while (taking[j]) {} while (priority(j)) {} } CS t[i]=0; 57 lalis@inf.uth.gr
Παρατηρήσεις Οι προηγούμενες λύσεις βασίζονται αποκλειστικά σε συμβατικό κώδικα (είναι γενικότερα εφαρμόσιμες) Χωρίς να γίνεται καμία (περιοριστική) υπόθεση εργασίας για την εκτέλεση των νημάτων Χωρίς να υπάρχει κανένας έλεγχος της εναλλαγής Χωρίς να γίνεται χρήση ειδικών εντολών υλικού Όμως: Για κάθε ΚΤ χρειάζεται ξεχωριστό σετ μεταβλητών Υπάρχει ενεργή αναμονή Ο αριθμός των ανταγωνιστικών νημάτων ανά ΚΤ πρέπει να είναι γνωστός εκ των προτέρων 58 lalis@inf.uth.gr
Ειδικές εντολές σε επίπεδο υλικού Οι επεξεργαστές προσφέρουν ειδικές ατομικές εντολές που μπορεί να χρησιμοποιηθούν για τον συγχρονισμό ανάμεσα σε νήματα με κοινή μνήμη Π.χ. test-and-set ή compare-and-swap int TAS(int *v) { int oldv; int CAS(int* v, int curv, int newv) { int oldv; } ATOMIC oldv=*v; *v=1; END_ATOMIC return(oldv); } ATOMIC oldv = *v; if (oldv == curv) *v=newv; END_ATOMIC return(oldv); 59 lalis@inf.uth.gr
Κρίσιμο τμήμα με TAS και CAS int lock=0; Pi: while (TAS(&lock)){} CS lock = 0; int lock=0; Pi: while (CAS(&lock,0,1)){} CS lock = 0; Η λύση ισχύει για άγνωστο αριθμό νημάτων Δεν εγγυάται πρόοδο γιατί; 60 lalis@inf.uth.gr
Χωρίς ενεργή αναμονή 61 lalis@inf.uth.gr
Ενεργή αναμονή (busy waiting) Κατά την αναμονή στον κώδικα εισόδου τα νήματα ξοδεύουν χρόνο του επεξεργαστή χωρίς νόημα «σπινάρουν» γύρω από την μια συνθήκη αναμονής που δεν μπορεί να αλλάξει όσο κρατάνε τον επεξεργαστή Αυτό καθυστερεί την πρόοδο των υπολοίπων νημάτων συμπεριλαμβανομένου και του νήματος που βρίσκεται μέσα στο ΚΤ Όσο περισσότερα νήματα εκτελούν ενεργή αναμονή, τόσο πιο αργά προοδεύει το νήμα που βρίσκεται μέσα στο ΚΤ και τόσο πιο αργά θα βγει από το ΚΤ 62 lalis@inf.uth.gr
P0 P1 P0: CS0 P0: CS0 P0: CS0 P1: while () P1: while () P1: while () P0: CS0 P0: CS0 P0: CS0 P1: while () P1: while () P1: while () P0: CS0 P0: CS0 P0: CS0 P1: while () 63 lalis@inf.uth.gr
Εθελοντική (ρητή) εναλλαγή Όσο ισχύει η συνθήκη αναμονής, το νήμα μπορεί να παραχωρήσει (εθελοντικά) την CPU ώστε να συνεχιστεί η εκτέλεση ενός άλλου νήματος που μπορεί να προοδεύσει, όπως το νήμα που βρίσκεται στο ΚΤ Ο χαμένος χρόνος της CPU μειώνεται σημαντικά χωρίς ρητή εναλλαγή while (<cond>) {} CS falsify <cond> με ρητή εναλλαγή while (<cond>) {yield();} CS falsify <cond> yield(); /* be nice */ 64 lalis@inf.uth.gr
P0 P1 P0: CS0 P0: CS0 P0: CS0 P1: while () yield() P0: CS0 P0: CS0 P0: CS0 P1: while () yield() P0: CS0 P0: CS0 P0: CS0 P1: while () yield() P0: CS0 P0: CS0 P0: CS0 P1: while () yield() P0: CS0 65 lalis@inf.uth.gr
Χωρίς ενεργή αναμονή Η ρητή παραχώρηση του επεξεργαστή δεν λύνει εντελώς το πρόβλημα της ενεργής αναμονής Απλά το κάνει λιγότερο επώδυνο Εξακολουθεί να χάνεται χρόνος του επεξεργαστή για τον έλεγχο της συνθήκης αναμονής και την εναλλαγή Το ιδανικό Τα νήματα να απενεργοποιούνται πλήρως, μέχρι να έρθει η σειρά τους να εισέλθουν στο ΚΤ 66 lalis@inf.uth.gr
Απαλοιφή ενεργής αναμονής Έστω ότι το σύστημα παρέχει τις λειτουργίες αναμονής και αφύπνισης WAIT / WAKEUP Η απαλοιφή της ενεργής αναμονής φαίνεται μάλλον εύκολη υπόθεση Αντί το νήμα να ελέγχει συνεχώς/ενεργά την συνθήκη, καλεί την WAIT και μπαίνει σε αναμονή Αντίστοιχα, όταν ένα νήμα αλλάζει την συνθήκη, αφυπνίζει με την WAKEUP το νήμα που περιμένει Όμως, λύσεις τέτοιου τύπου είναι ιδιαίτερα ευάλωτες σε συνθήκες ανταγωνισμού 67 lalis@inf.uth.gr
int locked=0; Pi: if (TAS(&locked)) { WAIT(); } γιατί αυτή η λύση δεν είναι σωστή; CS locked=0; WAKEUP(); 68 lalis@inf.uth.gr
int locked=0; Pi: while (TAS(&locked)) { WAIT(); } γιατί αυτή η λύση δεν είναι σωστή; CS locked=0; WAKEUP(); 69 lalis@inf.uth.gr
Υποστήριξη συγχρονισμού σε ψηλό επίπεδο Ο έλεγχος της εναλλαγής και η εγγύηση ατομικής εκτέλεσης κώδικα υπάρχει συνήθως ως δυνατότητα μόνο στο επίπεδο/περιβάλλον που υλοποιεί τους μηχανισμούς της ταυτόχρονης εκτέλεσης, π.χ., λειτουργικό, βιβλιοθήκη νημάτων, Με την σειρά τους, τα περιβάλλοντα εκτέλεσης παρέχουν στον προγραμματιστή άλλα «εργαλεία» συγχρονισμού: σηματοφόροι, ελεγκτές, 70 lalis@inf.uth.gr
Η ενεργή αναμονή μπορεί να είναι καλή ιδέα Σε συστήματα με πολλές CPU, η ενεργή αναμονή (μέσω spinlocks) είναι λιγότερο προβληματική Απλά χάνεται ο χρόνος της CPU που τρέχει το νήμα που πραγματοποιεί ενεργή αναμονή (χωρίς να υπάρχει το κόστος μιας εναλλαγής) όμως δημιουργείται κίνηση προς την μνήμη Αν η πιθανότητα ανταγωνισμού είναι μικρή και η παραμονή των νημάτων στο ΚΤ είναι σύντομη, η ενεργή αναμονή μπορεί να είναι πιο αποδοτική από αναστολή/επαναφορά νημάτων μέσω κλήσεων συστήματος 71 lalis@inf.uth.gr
P1 P2 L1 TAS lock,tmp cmp tmp,0 brneq L1 // CS store 0,lock CPU lock shared memory L1 TAS lock,tmp cmp tmp,0 brneq L1 // CS store 0,lock CPU P1 P2 lock unlock lock unlock lock unlock CS CS spin CS lock unlock lock unlock lock unlock spin CS CS CS Προγραμματισμός ΙΙΙ 72 lalis@inf.uth.gr