Προσπέλαση σύνθετων δομών δεδομένων χωρίς καθολικό κλείδωμα ΙΙΙ 1 lalis@inf.uth.gr
Προβλήματα με κλείδωμα Υπερβολική σειριοποίηση / άσκοπη αναμονή Κόστος συγχρονισμού Αδιέξοδα Απότομος τερματισμός νημάτων στο ΚΤ Απόδοση σε συστήματα πολυεπεξεργαστών ΙΙΙ 2 lalis@inf.uth.gr
Παράδειγμα: συνδεδεμένη λίστα Βασικές λειτουργίες: διάσχιση λίστας προς αναζήτηση κόμβου μεταβολή κόμβου προσθήκη κόμβου διαγραφή κόμβου typedef struct listnode { int key; int val; struct *listnode ; } listnode; listnode *head; head key 5 val 22 key 3 val 55 key 9 val 44 ΙΙΙ 3 lalis@inf.uth.gr
Κλείδωμα όλης της λίστας LOCK(&lock); // any type of access UNLOCK(&lock); typedef struct listnode { int key; int val; struct *listnode ; } listnode; listnode *head; lock_t lock; Δεν υποστηρίζεται ταυτόχρονη ανάγνωση/διάσχιση της λίστας Δεν υποστηρίζεται ανάγνωση/διάσχιση της λίστας την ίδια ώρα που γίνεται κάποια αλλαγή (σε διαφορετικό σημείο) Δεν υποστηρίζονται ταυτόχρονες αλλαγές σε διαφορετικά σημεία Αυξημένος βαθμός ανταγωνισμού: μεγάλη πιθανότητα ένα νήμα να μπλοκάρει (χωρίς κάποιο ουσιαστικό λόγο) ΙΙΙ 4 lalis@inf.uth.gr
Κλείδωμα για ανάγνωση και γράψιμο LOCK_READ(&lock); // read-only access UNLOCK_READ(&lock); LOCK_WRITE(&lock); // read-write access UNLOCK_WRITE(&lock); typedef struct listnode { int key; int val; struct *listnode ; } listnode; listnode *head; rwlock_t lock; Δεν υποστηρίζεται ταυτόχρονη ανάγνωση/διάσχιση της λίστας Δεν υποστηρίζεται ανάγνωση/διάσχιση της λίστας την ίδια ώρα που γίνεται κάποια αλλαγή σε διαφορετικό σημείο Δεν υποστηρίζονται ταυτόχρονες αλλαγές σε διαφορετικά σημεία Αυξημένος βαθμός ανταγωνισμού: μεγάλη πιθανότητα ένα νήμα να μπλοκάρει (χωρίς κάποιο ουσιαστικό λόγο) Εξακολουθεί να υπάρχει μη-αμελητέο κόστος συγχρονισμού για τους αναγνώστες: 2 κλήσεις ανά πρόσβαση ΙΙΙ 5 lalis@inf.uth.gr
Κλείδωμα ανά κόμβο if (cur->key!= k) { prev=cur; cur=cur->; LOCK_*(cur->lock); UNLOCK_*(prev->lock); } crabbing typedef struct listnode { rwlock_t lock; int key; int val; struct *listnode ; } listnode; listnode *head; Δεν υποστηρίζεται ταυτόχρονη ανάγνωση/διάσχιση της λίστας Δεν υποστηρίζεται ανάγνωση/διάσχιση της λίστας την ίδια ώρα που γίνεται κάποια αλλαγή (σε διαφορετικό σημείο) Δεν υποστηρίζονται ταυτόχρονες αλλαγές σε διαφορετικά σημεία Αυξημένος βαθμός ανταγωνισμού: μεγάλη πιθανότητα ένα νήμα να μπλοκάρει (χωρίς κάποιο ουσιαστικό λόγο) Μεγάλο κόστος συγχρονισμού για όλους: 2 κλήσεις για κάθε κόμβο που προσπελάζεται κατά την διάσχιση της λίστας Χρειάζεται προσοχή για να μην προκύψουν deadlocks! ΙΙΙ 6 lalis@inf.uth.gr
Πίσω στο βασικό ερώτημα Τι προσπαθούμε να πετύχουμε με τον αμοιβαίο αποκλεισμό, π.χ., με την τεχνική του κλειδώματος; Την ορθότητα της υλοποίησης! Να μην υπάρξει εκτέλεση το αποτέλεσμα της οποίας αντιφάσκει με τις προδιαγραφές λειτουργικότητας βλέπε επιτρεπτά ταυτόχρονα σενάρια εκτέλεσης Μπορεί να επιτευχθεί χωρίς αμοιβαίο αποκλεισμό; ΝΑΙ! (σε κάποιες περιπτώσεις) ΙΙΙ 7 lalis@inf.uth.gr
Τεχνικές χωρίς κλείδωμα υψηλού επιπέδου Βασίζεται σε ειδική υποστήριξη του υλικού/cpu Κλασική περίπτωση: ατομικές εντολές Οι πράξεις ανάγνωσης μπορεί να υλοποιηθούν χωρίς μπλοκάρισμα, με ιδιαίτερα αποδοτικό συγχρονισμό Οι πράξεις αλλαγής μπορεί να συγχρονιστούν μεταξύ τους με «φτηνό» κλείδωμα (spinlocks) ή με λογική best-effort και επανάληψη σε περίπτωση αποτυχίας Χρειάζεται προσοχή στην αλληλεπίδραση πράξεων «καταστροφής» (π.χ. αποδέσμευση μνήμης) και πράξεων ανάγνωσης ΙΙΙ 8 lalis@inf.uth.gr
Ειδική εντολή compare-and-swap int CAS(int* v, int curv, int newv) { int oldv; } ATOMIC oldv = *v; if (oldv == curv) *v=newv; END_ATOMIC return(oldv); ΙΙΙ 9 lalis@inf.uth.gr
Απλό παράδειγμα: αύξηση μεταβλητής συγχρονισμός με κλείδωμα LOCK(&lck); i = i + 1; UNLOCK(&lck); int old; συγχρονισμός χωρίς κλείδωμα do { old = i; } while (CAS(&i,old,old+1)!= old); ΙΙΙ 10 lalis@inf.uth.gr
Read-copy-update (RCU) Προσπέλαση σύνθετων αντικειμένων και δομών δεδομένων χωρίς κλείδωμα Οι αλλαγές γίνονται σε αντίγραφα των αντικειμένων Οι αλλαγές «μονιμοποιούνται» αντικαθιστώντας τα πρωτότυπα αντικείμενα με την αλλαγμένη έκδοση Η αντικατάσταση γίνεται ατομικά: ένας αναγνώστης θα δει είτε την παλιά είτε την καινούργια κατάσταση, ποτέ κάτι «ενδιάμεσο» Η ατομικότητα της αλλαγής εξασφαλίζεται με την ατομική αλλαγή ενός μοναδικού δείκτη Προς το παρόν, υποθέτουμε μόνο έναν writer ΙΙΙ 11 lalis@inf.uth.gr
Προσθήκη κόμβου head key 5 val 22 key 3 val 55 key 9 val 44 key 7 val 11 στοιχείο προς προσθήκη ΙΙΙ 12 lalis@inf.uth.gr
P1: διασχίζει την λίστα P2: προσθέτει έναν κόμβο if (cur->key!= key) cur=cur->; new=(listnode *)malloc( ); new->key=7; new->val=11; new->=cur->; cur->=new; any order! must be atomic & must come last! P1.cur head key 5 val 22 key 3 val 55 key 9 val 44 P2.cur P2.new key 7 val 11 ΙΙΙ 13 lalis@inf.uth.gr
head key 5 val 22 key 3 val 55 key 9 val 44 key 7 val 11 ο νέος κόμβος δεν είναι ακόμα προσπελάσιμος (1) head key 5 val 22 key 3 val 55 key 9 val 44 key 7 val 11 ο νέος κόμβος γίνεται μονομιάς προσπελάσιμος (2) ΙΙΙ 14 lalis@inf.uth.gr
Αλλαγή περιεχομένου ενός κόμβου Ένας κόμβος μπορεί να έχει πολλά ή σύνθετα πεδία (μεγαλύτερα από το word size της αρχιτεκτονικής) Οι αλλαγές στην μνήμη δεν είναι εγγυημένα ατομικές ούτε γίνονται απαραίτητα με την σειρά με την οποία είναι γραμμένες οι εντολές στον πηγαίο κώδικα Όλες οι αλλαγές γίνονται σε ένα αντίγραφο Η παλιά έκδοση του αντικειμένου αντικαθίσταται με την νέα έκδοση μονομιάς (βλέπε προσθήκη) ΙΙΙ 15 lalis@inf.uth.gr
head key 5 val 22 key 3 val 55 key 9 val 44 (1) δημιουργία καινούργιου κόμβου (με σκουπίδια) key? val?? head key 5 val 22 key 3 val 55 key 9 val 44 (2) αντιγραφή δεδομένων από τον κόμβο που θέλουμε να αλλάξουμε key 3 val 55 ΙΙΙ 16 lalis@inf.uth.gr
head key 5 val 22 key 3 val 55 key 9 val 44 (3) αλλαγή των δεδομένων στο αντίγραφο που δεν είναι ακόμα προσπελάσιμο key 3 val 11 head key 5 val 22 key 3 val 55 key 9 val 44 (4) η νέα έκδοση γίνεται μονομιάς προσπελάσιμη key 3 val 11 ΙΙΙ 17 lalis@inf.uth.gr
Αφαίρεση κόμβου Η αφαίρεση κόμβων υλοποιείται ως ειδική περίπτωση της αλλαγής των περιεχομένων ενός κόμβου Αντί να συμπεριληφθεί κάποια νέα έκδοση του αντικειμένου, απλά παρακάμπτεται η παλιά ΙΙΙ 18 lalis@inf.uth.gr
head key 5 val 22 key 3 val 55 key 9 val 44 (1) ο κόμβος είναι ακόμα προσπελάσιμος head key 5 val 22 key 3 val 55 key 9 val 44 (2) ο κόμβος δεν είναι πλέον προσπελάσιμος ΙΙΙ 19 lalis@inf.uth.gr
Τι πετύχαμε; Η διάσχιση της λίστας και η ανάγνωση των κόμβων μπορεί να γίνει ταυτόχρονα με την προσθήκη νέων και την αλλαγή/αφαίρεση κόμβων, χωρίς κλείδωμα Δραστική βελτίωση σε σχέση με προηγούμενες λύσεις Υπάρχουν όμως ακόμα διάφορα προβλήματα Ανακύκλωση μνήμης/κόμβων παλιά αντίγραφα κόμβων, κόμβοι που έχουν αφαιρεθεί Ταυτόχρονες αλλαγές προσθήκη/αλλαγή/αφαίρεση από πολλούς writers ΙΙΙ 20 lalis@inf.uth.gr
P1: διασχίζει την λίστα if (cur->key!= key) cur=cur->; P2: αφαιρεί έναν κόμβο prev->=cur->; head key 5 val 22 key 3 val 55 key 9 val 44 key 7 val 11 όλα μια χαρά, εκτός και αν ΙΙΙ 21 lalis@inf.uth.gr
P1: διασχίζει την λίστα if (cur->key!= key) cur=cur->; P2: αφαιρεί έναν κόμβο prev->=cur->; free(cur); P1 head key 5 val 22 key 3 val 55 key 9 val 44 key 7 val 11 την στιγμή που καταστρέφεται ο κόμβος, ίσως υπάρχουν ακόμα νήματα που τον χρησιμοποιούν ΙΙΙ 22 lalis@inf.uth.gr
Πρόβλημα Η μνήμη ενός κόμβου που αφαιρείται, πρέπει κάποτε να απελευθερωθεί ή να ανακυκλωθεί Δεν ξέρουμε αν/πόσα νήματα χρησιμοποιούν αυτόν τον κόμβο θα έπρεπε να γνωρίζουμε όλους τους δείκτες που υπάρχουν σε καθολικές και τοπικές μεταβλητές (και στην στοίβα) Δεν μπορούμε να αποφασίσουμε πότε να καταστρέψουμε τον κόμβο με ασφάλεια πρόβλημα «συλλογής απορριμμάτων» (garbage collection) Χρειαζόμαστε επιπλέον πληροφορία ΙΙΙ 23 lalis@inf.uth.gr
Read-side critical section Ένας αναγνώστης δηλώνει με ρητό τρόπο το πότε αρχίζει και το πότε τελειώνει το διάβασμα Έστω ότι για αυτό το σκοπό έχουμε τις λειτουργίες SYNC_READER_BEG() και SYNC_READER_END() δήλωση κρίσιμου τμήματος ανάγνωσης Εξακολουθεί να επιτρέπεται η ταυτόχρονη ανάγνωση Η δήλωση αφορά ουσιαστικά τους εγγραφείς ΙΙΙ 24 lalis@inf.uth.gr
SYNC_READER_BEG(); /* beginning of read-side CS */ list_node *cur=head; while ((cur!= NULL) && (cur->val!= key)) { cur=cur->next; } if (cur!= NULL) { // use as needed } SYNC_READER_STOP(); /* end of read-side CS */ ΙΙΙ 25 lalis@inf.uth.gr
reader P1 reader P2 reader P3 ΙΙΙ 26 lalis@inf.uth.gr
Καταστροφή αντικειμένων Ένα νήμα μπορεί να αλλάξει ή να απομακρύνει έναν κόμβο ταυτόχρονα με άλλα νήματα που διαβάζουν Βλέπε προηγούμενα Όμως, το χρονικό σημείο της αλλαγής μπορεί να βρει κάποια νήματα μέσα στο κρίσιμο τμήμα ανάγνωσης Αυτά τα νήματα πρέπει να ολοκληρώσουν την ανάγνωση, με βάση την παλιά έκδοση της λίστας Η καταστροφή του αλλαγμένου/απομακρυσμένου αντικειμένου πρέπει να γίνει αφού κάθε τέτοιο νήμα βγει από το κρίσιμο τμήμα ανάγνωσης ΙΙΙ 27 lalis@inf.uth.gr
reader P1 reader P2 reader writer P3 P4 swap garbage collection ΙΙΙ 28 lalis@inf.uth.gr
Συγχρονισμός εγγραφέα-αναγνωστών Πως ξέρει o εγγραφέας ότι κάθε ανταγωνιστικός αναγνώστης έχει βγει από το κρίσιμο τμήμα του; Έστω ότι για αυτόν τον σκοπό έχουμε την λειτουργία SYNC_WRITER_WAIT() που μπλοκάρει μέχρι να μην υπάρχουν (άλλοι) ανταγωνιστικοί αναγνώστες Αφού επιστρέψει το SYNC_WRITER_WAIT(), ο εγγραφέας απελευθερώνει ή ανακυκλώνει την μνήμη του αντικειμένου που άλλαξε/αφαίρεσε Δεν υπάρχει καμία περίπτωση αυτό να δημιουργήσει πρόβλημα σε κάποιο αναγνώστη ΙΙΙ 29 lalis@inf.uth.gr
list_node *cur=head, *prv=null; while ((cur!= NULL) && (cur->val!= key)) { prv=cur; cur=cur->next; } if (val!= -1) { new=(listnode *)malloc( ); new->key=key; new->val=11; // can be complex and non-atomic new->=cur->; if (prv == NULL) { head=new; } else { prv->=new; } SYNC_WRITER_WAIT(); /* wait for readers to finish */ free(cur); } ΙΙΙ 30 lalis@inf.uth.gr
Υλοποίηση συγχρονισμού Λύση Α Την στιγμή της αλλαγής, καταγράφουμε τα νήματα που βρίσκονται στο κρίσιμο τμήμα ανάγνωσης, και περιμένουμε μέχρι να βγουν από το κρίσιμο τμήμα Πρέπει να διαχειριζόμαστε πληροφορία για κάθε ΚΤ (μήπως αναπαράγουμε το πρόβλημα που πάμε να λύσουμε;) Λύση Β Δεν επιτρέπουμε το μπλοκάρισμα ούτε την εναλλαγή για νήματα που βρίσκονται μέσα στο ΚΤ ανάγνωσης Αρκεί να περιμένουμε να γίνει 1 εναλλαγή ανά core Αν το σύστημα έχει μόνο 1 core, υπάρχει αμοιβαίος αποκλεισμός και ανάμεσα στους αναγνώστες ΙΙΙ 31 lalis@inf.uth.gr
Υλοποίηση (συμβατικό μοντέλο μνήμης) λειτουργίες reader void SYNC_READER_BEG() { preemption_disable(); } void SYNC_READER_END() { preemption_enable(); } λειτουργίες writer void SYNC_WRITER_WAIT() { #ifdef MULTICORE int i; for (i=0; i<nof_cores; i++) { switch_to_core(i); // do nothing } #else // do nothing #endif όταν λάβω τον έλεγχο, είναι πλέον σίγουρο ότι δεν υπάρχουν ανταγωνιστικοί αναγνώστες στο τοπικό core που τρέχω και εγώ } δεν τίθεται θέμα ανταγωνισμού αν δεν υπάρχουν πολλά cores ΙΙΙ 32 lalis@inf.uth.gr
Μη συμβατικό μοντέλο μνήμης Παρασκηνιακές αλλαγές στην σειρά με την οποία περνάνε στην μνήμη οι αλλαγές που κάνει το πρόγραμμα στις μεταβλητές του Π.χ. delayed memory writes Πρόβλημα για τις αλληλεπιδράσεις μεταξύ νημάτων, ιδίως όταν εκτελούνται σε ξεχωριστούς επεξεργαστές Λύση: ρητός συγχρονισμός μνήμης μέσω λειτουργιών χαμηλού επιπέδου Συνηθισμένος μηχανισμός: fences/barriers ΙΙΙ 33 lalis@inf.uth.gr
Υλοποίηση (μη συμβατικό μοντέλο μνήμης) λειτουργίες reader void SYNC_READER_BEG() { preemption_disable(); } void SYNC_READER_END() { preemption_enable(); } #define sync_rd_ptr(p) ({ // read pointer // at a read barrier }) λειτουργίες writer void SYNC_WRITER_WAIT() { #ifdef MULTICORE int i; for (i=0; i<nof_cores; i++) { switch_to_core(i); // do nothing } #else // nothing #endif } #define sync_asgn_ptr(p, v) ({ // write pointer // at a write barrier }) ΙΙΙ 34 lalis@inf.uth.gr
reader's code SYNC_READER_BEG(); /* beginning of read-side CS */ list_node *cur=sync_rd_ptr(head); while ((cur!= NULL) && (cur->val!= key)) { cur=sync_rd_ptr(cur->next); } if (cur!= NULL) { // use as needed } SYNC_READER_STOP(); /* end of read-side CS */ ΙΙΙ 35 lalis@inf.uth.gr
writer's code list_node *cur=head, *prv=null; while ((cur!= NULL) && (cur->key!= value)) { prv=cur; cur=cur->next; } if (val!= -1) { new=(listnode *)malloc( ); new->key=key; new->val=11; // can be complex and non-atomic new->=cur->; /* force writes before crucial ptr redirection */ if (prv == NULL) { sync_asgn_ptr(head,new); } else { sync_asgn_ptr(prv->,new); SYNC_WRITER_WAIT(); /* wait for readers to finish */ free(cur); } ΙΙΙ 36 lalis@inf.uth.gr
Πολλοί εγγραφείς Οι προηγούμενες λύσεις δουλεύουν αν υπάρχει μόνο ένα νήμα που αλλάζει την κατάσταση Τι γίνεται αν πολλά νήματα επιχειρήσουν να αλλάξουν την κατάσταση ταυτόχρονα; Λύση Α: συμβατικός αμοιβαίος αποκλεισμός ανάμεσα στους εγγραφείς (spinlocks, mutexes) Λύση Β: η «ατομική» αλλαγή (με ανάθεση του δείκτη) επιχειρείται με μια ειδική εντολή υλικού, π.χ. CAS αν εντοπιστεί παρεμβολή, ο εγγραφέας επαναλαμβάνει την προσπάθεια από την αρχή ΙΙΙ 37 lalis@inf.uth.gr
Παράδειγμα: προσθήκη νέου κόμβου P2.new P2.cur key 6 val 99 αν δύο νήματα επιχειρήσουν να εισάγουν έναν κόμβο στο ίδιο σημείο ταυτόχρονα, θα επικρατήσει το τελευταίο head key 5 val 22 key 3 val 55 key 9 val 44 P2.cur P2.new key 7 val 11 ο κόμβος που προστέθηκε από το πρώτο νήμα θα παραμείνει απροσπέλαστος ΙΙΙ 38 lalis@inf.uth.gr
προσθήκη νέου κόμβου με κλείδωμα new=(listnode *)malloc( ); new->key=key; new->val=11; LOCK(&lock); // find place to add new->=cur->; cur->=new; UNLOCK(&lock); προσθήκη νέου κόμβου χωρίς κλείδωμα new=(listnode *)malloc( ); new->key=key; new->val=11; do { // find place to add new->=cur->; υπάρχει (θεωρητική) πιθανότητα λιμοκτονίας } while (CAS(&cur->,new->,new)!= new->); ΙΙΙ 39 lalis@inf.uth.gr
Υποθέσεις και περιορισμοί RCU Πολλοί περισσότεροι αναγνώστες από εγγραφείς εξακολουθεί να υπάρχει συγχρονισμός ανάμεσα σε writers Αποκλειστική χρήση core στα read critical sections η διάρκεια του ΚΤ ανάγνωσης πρέπει να είναι μικρή Συγχρονισμός πάνω σε μεμονωμένα αντικείμενα Δεν παρέχονται ευρύτερες εγγυήσεις συνέπειας σε συνολικό επίπεδο ολόκληρης της δομής δεδομένων αυτό ίσως να μην είναι πάντα αποδεκτό πρέπει να ελέγχονται προσεκτικά οι απαιτήσεις ΙΙΙ 40 lalis@inf.uth.gr