2.4 Κλασσικά Προβλήματα IPC 1
Οι φιλόσοφοι που γευματίζουν - Dining Philosophers Μια πρώτη λύση για Ν φιλοσόφους: philosopher (i) while (1) { think; take_fork(i);/* πάρε αριστερό ξυλάκι */ take_fork(i+1 mod Ν); /* πάρεδεξί ξυλάκι */ eat; put_fork(i); put_fork(i+1 mod Ν); } 2
Οι φιλόσοφοι που γευματίζουν - Dining Philosophers Ο παραπάνω αλγόριθμος δεν ικανοποιεί! Αν όλοι οι φιλόσοφοι πεινάσουν την ίδια στιγμή και εκτελέσουν τον αλγόριθμο τότε όλοι θα πάρουν το πηρούνι στα αριστερά τους. Όμως κανείς δεν θα καταφέρει να πάρει το δεξί πηρούνι όλοι θα... πεθάνουν της πείνας. Αυτό είναι ένα γενικότερο και πολύ σημαντικό πρόβλημα για την δημιουργία συστημάτων λογισμικού και λέγεται αδιέξοδο (deadlock). 3
Οι φιλόσοφοι που γευματίζουν - Dining Philosophers Αδιέξοδο προκύπτει όταν >1 processes δημιουργούν μια κυκλική αλυσίδα: όπου το κάθε process για να συνεχίσει χρειάζεται έναν πόρο που τον κατέχει το επόμενο process... 4
Οι φιλόσοφοι που γευματίζουν - Dining Philosophers Μια 2 η λύση είναι ν' αναγκάσουμε τον κάθε φιλόσοφο, μόλις πάρει το αριστερό ξυλάκι να εξετάσει αν το δεξί είναι διαθέσιμο. Αν ναι, εντάξει. Αλλιώς, αφήνει το αριστερό ξυλάκι και δοκιμάζει πάλι μετά από κάποιο χρονικό διάστημα. Το πρόβλημα μ' αυτή τη λύση είναι λίγο διαφορετικό. Αν όλοι οι φιλόσοφοι αρχίσουν την ίδια στιγμή, ενδέχεται να πάρουν το αριστερό την ίδια στιγμή, να τ' αφήσουν την ίδια στιγμή κ.ο.κ. Αυτό το φαινόμενο ονομάζεται επ αόριστον αναβολή (indefinite postponement) ή λιμοκτονία (starvation): οι διεργασίες συνεχίζουν να τρέχουν αλλά 5 δεν σημειώνουν καμία πρόοδο...
Οι φιλόσοφοι που γευματίζουν - Dining Philosophers Μια 3 η λύση: να δημιουργήσουμε ένα critical section στον αλγόριθμο των φιλοσόφων (μετά το "think") ο οποίος να προστατεύεται από ένα semaphore (mutex = 1). Πριν μπεί στο critical section ο κάθε φιλόσοφος καλεί DOWN(mutex) και μόλις βγει καλεί UP(mutex). 6
Οι φιλόσοφοι που γευματίζουν - Dining Philosophers mutex = 1; philosopher (i) while (1) {think; down(mutex); take_fork(i);/* πάρε αριστερό ξυλάκι */ take_fork(i+1 mod Ν); /* πάρεδεξί ξυλάκι */ eat; put_fork(i); put_fork(i+1 mod Ν); up(mutex); } Η λύση αυτή είναι σωστή; Είναι καλή λύση ; 7
Οι φιλόσοφοι που γευματίζουν - Dining Philosophers Μια καλύτερη λύση: Χρησιμοποιείται ένα semaphore (mutex = 1) για προστασία critical section. Χρησιμοποιείται ένα semaphore για κάθε φιλόσοφοπ.χ. s[1..ν], για Ν φιλόσοφους. Αρχικά s[i] = 0, i= 1,...,Ν. Τέλος υπάρχει και μία μεταβλητή που αντιπροσωπεύει την κατάσταση ενός φιλόσοφουstate[i] (με τιμές : ΣΚΕΦΤΕΤΑΙ, ΠΕΙΝΑΕΙ, ΤΡΩΕΙ). Απαίτηση: Για να φάει ένας φιλόσοφος, κανείς από τους 2 γείτονές του δεν μπορεί να βρίσκεται στην κατάσταση ΤΡΩΕΙ. 8
Οι φιλόσοφοι που γευματίζουν - Dining Philosophers Η βασική ιδέα: Για να μην μπλοκάρουν όλοι χωρίς λόγο, κάθε φιλόσοφος i θα μπλοκάρει στη δική του σημαφόροs[i]. Επειδή παράλληλα κάποιοι φιλόσοφοι θα εξετάζουν την κατάσταση (state) των άλλων και θα αλλάζουν την κατάστασή τους, θα χρησιμοποιηθεί μια σημαφόρος για αυτό (mutex). 9
Οι φιλόσοφοι που γευματίζουν - Dining Philosophers philosopher (i) while (1) { think; take_forks(i); eat; put_forks(i); } test (i) if (state[i] == hungry &&state[pred]!= eating &&state[succ]!= eating) { state[i] = eating; up (s[i]); } take_forks(i) { put_forks(i) down (mutex); down (mutex); state[i]: = hungry; state[i]: = thinking; test (i); test (PRED); up (mutex); test (SUCC); down (s[i]); up (mutex); } } 10
Προβλήµατα ιαδιεργασιακής Επικοινωνίας Έστω ένα σύστηµα στο οποίο ένα σύνολο από διεργασίες-πελάτες (clients) επικοινωνούν µε µία διεργασία-εξυπηρέτη (server) µέσω µίας κοινής µεταβλητής X. Κάθε πελάτης µπορεί να παράγει επαναληπτικά αιτήσεις τις οποίες γράφει στην X προκειµένου να τις διαβάσει και να τις επεξεργαστεί αργότερα ο εξυπηρέτης. Κάθε φορά που ένας πελάτης παράγει µία αίτηση πρέπει να ενεργοποιηθεί ο εξυπηρέτης ώστε να εξυπηρετήσει την αίτηση, ενώ ενδιάµεσα κανένας άλλος πελάτης δεν θα πρέπει να µπορεί να παράγει αιτήσεις. Ο εξυπηρέτης µε τη σειρά του, αφού εξυπηρετήσει την αίτηση, θα πρέπει να ενεργοποιήσει κάποιον άλλον πελάτη (ή ίσως και τον ίδιο) για την παραγωγή µίας νέας αίτησης. Ζητείται να επιλυθεί το παραπάνω πρόβληµα συγχρονισµού χρησιµοποιώντας σηµαφόρους 11
ΣΚΕΨΕΙΣ ΕΠΙΛΥΣΗΣ 12 Το πρώτο ερώτηµα που θα πρέπει να απαντηθεί, όταν ζητείται λύση σε ένα πρόβληµα συγχρονισµού, είναι τι είδους διεργασίες αναµιγνύονται; Η απάντηση στο ερώτηµα αυτό είναι εύκολη και συνήθως προκύπτει άµεσα από την εκφώνηση. Αν ωστόσο δεν είναι σαφής, ο αναγνώστης θα πρέπει να απαντήσει την εξής ερώτηση: «Πόσα διαφορετικά προγράµµατα µπορούν να εκτελούνται ταυτόχρονα;» ή «Πόσες διαφορετικές οντότητες µπορούν να είναι ενεργές ταυτόχρονα;» ή «Πόσες διαφορετικές ενέργειες µπορούν να συµβαίνουν ταυτόχρονα;». Για κάθε ένα τέτοιο πρόγραµµα/οντότητα/ενέργεια, υπάρχει και ένα είδος διεργασιών. Για παράδειγµα, στο πρόβληµα που προαναφέρθηκε, υπάρχουν δύο είδη διεργασιών, η διεργασία εξυπηρέτη και οι διεργασίες πελατών. Άρα, θα πρέπει να γραφεί κώδικας για δύο ρουτίνες, µία για τη διεργασία εξυπηρέτη και µία για τις διεργασίες πελατών.
ΣΚΕΨΕΙΣ ΕΠΙΛΥΣΗΣ (συν.) Το δεύτερο ερώτηµα που χρειάζεται να απαντηθεί είναι αν απαιτείται η χρήση κοινών µεταβλητών (ή κοινών πόρων) που θα πρέπει να προσπελάζονται ατοµικά από τις διεργασίες. Για κάθε κοινό πόρο που απαιτείται, συνήθως χρειαζόµαστε έναν δυαδικό σηµαφόρο για να επιτύχουµε αµοιβαίο αποκλεισµό. Στο παράδειγµα µας, έχουµε µόνο µία κοινή µεταβλητή και άρα ενδεχόµενα χρειαζόµαστε έναν δυαδικό σηµαφόρο για να επιτύχουµε αµοιβαίο αποκλεισµό. Ας ονοµάσουµε τον σηµαφόρο αυτό mutex. 13
ΣΚΕΨΕΙΣ ΕΠΙΛΥΣΗΣ (συν.) Στη συνέχεια θα πρέπει να αναρωτηθούµε µήπως υπάρχουν καταστάσεις (ή περιπτώσεις), επιπρόσθετα εκείνων που προκύπτουν λόγω του αµοιβαίου αποκλεισµού, στις οποίες κάποιο είδος διεργασιών πρέπει να απενεργοποιείται. Προφανώς, κάθε διεργασία πρέπει να απενεργοποιείται όταν κάποια άλλη διεργασία προσβαίνει τον κοινό πόρο, αλλά µήπως υπάρχουν και άλλες περιπτώσεις που απαιτείται απενεργοποίηση διεργασιών στο πρόβληµά µας; 14
ΣΚΕΨΕΙΣ ΕΠΙΛΥΣΗΣ (συν.) Παρατηρούμε ότι η διεργασία εξυπηρέτη θα πρέπει να εκτελείται αυστηρά εναλλάξ µε µια κάθε φορά από τις διεργασίες πελατών. Παρατηρήστε ότι, ενώ η χρήση του δυαδικού σηµαφόρου mutex εγγυάται ότι µια µόνο διεργασία εκτελεί κάθε φορά το κρίσιμο τµήµα, δεν παρέχει καµία εγγύηση για τη σειρά µε την οποία θα εκτελεστούν οι διεργασίες. Βάσει των παραπάνω, προκύπτει ότι υπάρχουν δύο ακόµη περιπτώσεις στο πρόβλημα που µελετάµε, στις οποίες κάποιο είδος διεργασίας θα πρέπει να απενεργοποιείται: 1. Η διεργασία εξυπηρέτη πρέπει να απενεργοποιείται όσο δεν έχει εκτελεστεί κάποια από τις διεργασίες πελατών από την τελευταία φορά εκτέλεσής της. 2. Οι διεργασίες πελατών θα πρέπει να απενεργοποιούνται όσο η διεργασία εξυπηρέτη δεν έχει εκτελεστεί από την τελευταία φορά εκτέλεσης κάποιας διεργασίας πελάτη. 15
ΣΚΕΨΕΙΣ ΕΠΙΛΥΣΗΣ (συν.) Ο µοναδικός τρόπος απενεργοποίησης των διεργασιών είναι µε την εκτέλεση µιας λειτουργίας down() πάνω σε κάποιο σηµαφόρο του οποίου η τιµή είναι 0. Άρα, η λύση χρειάζεται τουλάχιστον τόσους σηµαφόρους όσες και οι περιπτώσεις που προαναφέρθηκαν. Ας ονοµάσουµε ServerSem το σηµαφόρο πάνω στον οποίο θα απενεργοποιούνται οι διεργασίες-πελάτες και ας ονοµάσουµε ClientSem το σηµαφόρο πάνω στον οποίο θα απενεργοποιείται η διεργασία εξυπηρέτης. 16
ΣΚΕΨΕΙΣ ΕΠΙΛΥΣΗΣ (συν.) Τώρα που έχουµε αποφασίσει πόσους σηµαφόρους θα χρησιµοποιήσουµε για να λύσουµε το πρόβληµα, το επόµενο βήµα είναι να περιγράψουµε µε λόγια τις ενέργειες που πρέπει να κάνει κάθε διεργασία. Καταρχήν, θα πρέπει να αποφασιστεί αν κάθε είδος διεργασιών εκτελεί επαναληπτικά τις ενέργειες που πρέπει να εκτελέσει ή όχι. Στην περίπτωση που οι ενέργειες εκτελούνται επαναληπτικά, θα πρέπει να αποτελούν το µπλοκ εντολών µιας εντολής ανακύκλωσης της µορφής repeat forever. Για το παράδειγµα µας, τόσο η διεργασία εξυπηρέτη όσο και οι διεργασίες πελατών εκτελούν επαναληπτικά κάποιες ενέργειες. Οι ενέργειες που εκτελεί κάθε διεργασία παρουσιάζονται παρακάτω. 17
ΣΚΕΨΕΙΣ ΕΠΙΛΥΣΗΣ (συν.) ιεργασία Εξυπηρέτης repeat begin 1. Αν η αίτηση δεν είναι έτοιµη απενεργοποιήσου; 2. ιάβασε την αίτηση από τη κοινή µεταβλητή Χ; τµήµα */ /* κρίσιµο 3. Ενηµέρωσε πελάτες για να ξεκινήσουν τη δηµιουργία νέας αίτησης; 4. Επεξεργάσου την αίτηση; /* µη-κρίσιµο τµήµα */ end forever; ιεργασία Πελάτης repeat begin 1. ηµιούργησε νέα αίτηση /* µη-κρίσιµο τµήµα */ 2. Όσο ο εξυπηρέτης διαβάζει µια αίτηση απενεργοποιήσου; 3. Τοποθέτησε την νέα αίτηση στη κοινή µεταβλητή Χ;/* κρίσιµο τµήµα */ 4. Ενηµέρωσε εξυπηρέτη; end 18 forever;
Client-Server (1 η Λύση) Η ρουτίνα process_request() αναλαµβάνει να επεξεργαστεί την αίτηση (µη-κρίσιµο τµήµα διεργασίας εξυπηρέτη) Η ρουτίνα produce_request() αναλαµβάνει την δηµιουργία µιας νέας αίτησης σε κάποια τοπική µεταβλητή της εκάστοτε διεργασίας πελάτη που την καλεί (µη-κρίσιµο τµήµα διεργασίας πελάτη) Κοινές µεταβλητές & Σηµαφόροι: semaphore ClientSem = 1; semaphore ServerSem = 0; semaphore mutex = 1; request X; repeat begin down(serversem); down(mutex); read X; up(mutex); up(clientsem); process_request(); end forever; ιεργασία-εξυπηρέτης repeat begin produce_request(); down(clientsem); down(mutex); write X; up(mutex); up(serversem); end forever; ιεργασία-πελάτης 19
Παρατηρήσεις 20 Χρειάζονται όλοι οι σηµαφόροι που χρησιµοποιήσαµε ή µήπως κάποιοι από αυτούς είναι περιττοί; Μήπως ο αµοιβαίος αποκλεισµός που απαιτείται για την ατοµική προσπέλαση στην διαµοιραζόµενη µεταβλητή X παρέχεται ήδη (έµµεσα) λόγω της χρήσης των υπολοίπων σηµαφόρων που έχουν στρατευτεί; Με άλλα λόγια, είναι ποτέ δυνατό δύο ή περισσότερες διεργασίες να προσπαθήσουν ταυτόχρονα να προσβούν την µεταβλητή Χ, ή µήπως η διαδιεργασιακή επικοινωνία που επιτυγχάνεται µε τη χρήση των σηµαφόρων ServerSem και ClientSem καθιστά κάτι τέτοιο αδύνατο; εν είναι δύσκολο να γίνει κατανοητό ότι η χρήση του σηµαφόρου mutex είναι περιττή. Όπως έχει ήδη αναφερθεί, η χρήση των σηµαφόρων ServerSem και ClientSem συνεπάγεται ότι η διεργασία εξυπηρέτης και µία κάθε φορά από τις διεργασίες πελατών θα εκτελούνται αυστηρά εναλλάξ. Εποµένως, η χρήση του mutex είναι περιττή. Με άλλα λόγια, ο αµοιβαίος αποκλεισµός κατά την προσπέλαση στην Χ είναι εγγυηµένος ακόµη και αν παραληφθούν οι εντολές down(mutex) και up(mutex) από τον κώδικα.
Η απλούστερη λύση του προβλήµατος client-server Κοινές µεταβλητές & Σηµαφόροι: semaphore ClientSem = 1; semaphore ServerSem = 0; request X; repeat begin ιεργασία-εξυπηρέτης down(serversem); read X; up(clientsem); process_request(); end forever; repeat begin produce_request(); down(clientsem); write X; up(serversem); end forever; ιεργασία-πελάτης 21
Το Πρόβληµα των Αναγνωστών- Εγγραφέων 22 Θεωρήστε βάση δεδοµένων της οποίας τα δεδοµένα µπορούν να αναγνωσθούν ή να τροποποιηθούν (εγγραφούν). Η ανάγνωση µπορεί να γίνεται ταυτόχρονα από πολλούς αναγνώστες αλλά προκειµένου να γίνει εγγραφή δεν επιτρέπεται κανένας άλλος αναγνώστης ή εγγραφέας να χρησιµοποιεί τη βάση (που είναι ο κοινός πόρος). Παρουσιάστε λύση στο πρόβληµα του αµοιβαίου αποκλεισµού χρησιµοποιώντας σηµαφόρους. Προφανώς υπάρχουν δύο είδη διεργασιών στο σύστηµα, οι διεργασίες αναγνωστών και οι διεργασίες εγγραφέων. Το πρόβληµα είναι αρκετά απλό. Αναγνώστες και εγγραφείς πρέπει να προσβαίνουν ατοµικά τη βάση δεδοµένων. Απαιτείται εποµένως ένας σηµαφόρος DataBase που θα χρησιµοποιηθεί για αµοιβαίο αποκλεισµό. εν υπάρχουν άλλες καταστάσεις στις οποίες κάποιο είδος διεργασιών θα πρέπει να απενεργοποιείται. Μια πρώτη προσπάθεια επίλυσης του προβλήµατος φαίνεται παρακάτω.
Το Πρόβλημα Αναγνωστών/Συγγραφέων (Readers & Writers) Χρησιμοποιείται κυρίως για την μοντελοποίηση προβλημάτων ταυτόχρονης πρόσβασης διαφορετικού τύπου. Πολλοί αναγνώστες (readers) μπορούν ταυτόχρονα να προσπελαύνουν ένα αντικείμενο. Αν όμως το αντικείμενο προσπελαύνεται από έναν συγγραφέα, τότε κανείς άλλος (reader ή writer) δεν μπορεί να προσπελάσει το αντικείμενο. π.χ. Τραπεζικοί λογαριασμοί = data items Ερωτήσεις για ύψος υπολοίπου = readers αναλήψεις, καταθέσεις = writers 23
Το Πρόβλημα Αναγνωστών/Συγγραφέων (Readers & Writers) 2 σηματοφόροι: ένας (mutex) να προστατεύει το read_countκαι ένας (item) για τη read-write και write-write σύγκρουση. Reader writer while (1) { while (1) { down (mutex); create_data(); read_count ++; down (item) ; if (read_count==1) write_item(); down (item); up (item) up (mutex) ; } read_item(); down (mutex); read_count --; if (read_count == 0) up (item); up (mutex); } Αρχικά, mutex = 1 και item = 1 και read_count = 0. Υπάρχει κάποιο πρόβλημα μ' αυτήτηνλύση; (ο γραφέας περιμένει) 24