Έννοιες, χρησιμότητα, προγραμματιστική διασύνδεση, συγχρονισμός Νήματα ελέγχου Έννοιες, χρησιμότητα, προγραμματιστική διασύνδεση, συγχρονισμός
Τι είναι ένα νήμα ελέγχου Μία ανεξάρτητη ροή εντολών που μπορεί να χρονοπρογραμματιστεί ανεξάρτητα από το λειτουργικό σύστημα Κατανοητός ορισμός: Κάθε διεργασία ξεκινά έχοντας ακριβώς ένα νήμα, αυτό που εκτελεί τις εντολές της main Ένα νήμα εντός μιας διεργασίας μπορεί να δημιουργεί άλλα νήματα ελέγχου, ουσιαστικά «ανεξάρτητες εκτελέσεις συναρτήσεων» Όλα τα νήματα εκτελούνται στον ίδιο χώρο διευθύνσεων (της διεργασίας) – οι πόροι της διεργασίας μοιράζονται μεταξύ των νημάτων της Τα νήματα συγχρονίζονται μεταξύ τους για πρόσβαση σε διαμοιραζόμενους πόρους Ένα νήμα τερματίζεται εκούσια (τελειώνει η ροή εντολών) ή ακούσια («σκοτώνεται» από άλλο νήμα) Τερματισμός της διεργασίας τερματίζει όλα τα νήματά της
Διαχείριση μνήμης Χωρίς νήματα Με νήματα Στοίβα νήματος 1 routine1 στοίβα σωρός Μη αρχ. Δεδομένα (bss) Αρχ. δεδομένα Κώδικας Στοίβα νήματος 1 routine1 στοίβα 1 routine1() v1, v2, v3 στοίβα 2 routine2() var1, var2, … σωρός Μη αρχ. Δεδομένα (bss) Αρχ. δεδομένα Κώδικας main routine1 routine2 Στοίβα νήματος 2 routine2 Χωρίς νήματα Με νήματα
Τα νήματα σε μία διεργασία… Μοιράζονται Κώδικα Καθολικές μεταβλητές Ανοικτά αρχεία Θυγατρικές διεργασίες Εκκρεμή σήματα Σήματα και χειριστές σημάτων Λογιστική Δεν μοιράζονται Δείκτη εντολών προγράμματος Καταχωρητές Στοίβα Ροή εκτέλεσης και τοπικές μεταβλητές Κατάσταση
Τα νήματα σε μία διεργασία... Εκτελούνται στον ίδιο χώρο διευθύνσεων Αλλαγές σε πόρους επιπέδου διεργασίας από ένα νήμα (π.χ. κλείσιμο αρχείων, αλλαγή τιμής καθολικής μεταβλητής κ.λπ.) αντανακλώνται άμεσα στα υπόλοιπα νήματα Δύο δείκτες με τις ίδιες τιμές δείχνουν στην ίδια διεύθυνση μνήμης Είναι δυνατόν να διαβάζονται και να γράφονται οι ίδιες διευθύνσεις μνήμης (καθολικές μεταβλητές ή έμμεσα με χρήση δεικτών) πάντα όμως με ρητό συγχρονισμό από τον προγραμματιστή
Γιατί νήματα; Πλεονεκτούν έναντι των αυτόνομων διεργασιών ως προς: χρόνο δημιουργίας (βλ. πίνακα δημιουργίας 50.000 διεργασιών/νημάτων) Πλατφόρμα fork() pthread_create() real user sys IBM 332 MHz 604e (332 MHz, 4 CPUs/node, 512 MB memory, AIX 4.3) 92.4 2.7 105.3 8.7 4.9 3.9 IBM 375 MHz Power 3 (375 MHz, 16 CPUs/node, 16 GB memory, AIX 5.1) 173.6 13.9 172.1 9.6 3.8 6.7 INTEL 2.2 GHz Xeon (2 CPU/node, 2 GB Memory, RedHat Linux 7.3) 17.4 13.5 5.9 0.8 5.3 Με το διαμοιρασμό μνήμης, η επικοινωνίας μεταξύ νημάτων είναι πιο εύκολη και πιο αποτελεσματική έναντι της διαδιεργασιακής επικοινωνίας Διατήρηση όλων των πλεονεκτημάτων των ξεχωριστών διεργασιών (επικάλυψη εισόδου/εξόδου και επεξεργασίας, προτεραιότητες επιπέδου νημάτων, χειρισμός ασύγχρονων συμβάντων [π.χ. υποδοχή αιτήσεων και επεξεργασία τους σε έναν εξυπηρέτη διαδικτύου])
Πακέτο pthreads Αρχικά ο κάθε κατασκευαστής λειτουργικού συστήματος έδινε τον δικό του τρόπο για δημιουργία και διαχείριση νημάτων άλλοι με υποστήριξη σε επίπεδο πυρήνα, άλλοι σε επίπεδο βιβλιοθήκης Προβληματική έως αδύνατη μεταφερσιμότητα Το πρότυπο POSIX όρισε έναν τυποποιημένο τρόπο δημιουργίας και διαχείρισης νημάτων στην έκδοση IEEE POSIX 1003.1c (1995) – POSIX Threads ή pthreads Σύνολο τύπων δεδομένων και διαδικασιών που ορίζονται στο αρχείο ενσωμάτωσης pthread.h Οι περισσότεροι κατασκευαστές το υποστηρίζουν, εξακολουθούν όμως να παρέχουν και τα δικά τους πακέτα νημάτων
Σχεδιασμός προγραμμάτων με νήματα Οργάνωση σε διακριτά ανεξάρτητα τμήματα που μπορούν να εκτελεστούν παράλληλα Καλοί υποψήφιοι είναι οι εργασίες που: μπορούν να ανασταλούν επί μακρόν Καταναλώνουν πολλή ΚΜΕ Πρέπει να χειρίζονται ασύγχρονα συμβάντα Είναι υψηλότερης ή χαμηλότερης σημασίας σε σχέση με άλλες εργασίες Μπορούν να εκτελεσθούν παράλληλα με άλλες εργασίες Προσοχή στη χρήση βιβλιοθηκών χωρίς τον χαρακτηρισμό MT-safe
Μοντέλα χρήσης νημάτων Συντονιστής - Υποτελείς Σωλήνωση Ομότιμα νήματα (π.χ. συλλογή απορριμάτων, εγγραφή δεδομένων, επεξεργασία κ.λπ.) Συντονιστής Στάδια σωλήνωσης Υποτελείς Διαμοιραζόμενοι πόροι Διαμοιραζόμενοι πόροι Διαμοιραζόμενοι πόροι
Δημιουργία νημάτων int pthread_create (pthread_t *, const pthread_attr_t *, void *(*)(void *), void *); Κάθε νήμα στα πλαίσια μιας διεργασίας προσδιορίζεται από έναν προσδιοριστή νήματος που αποδίδεται από το σύστημα. Η τιμή του επιστρέφεται στην 1η παράμετρο Κάθε νήμα έχει ένα σύνολο από χαρακτηριστικά που προσδιορίζονται μέσω της 2ης παραμέτρου. Αν δώσουμε τιμή NULL, χρησιμοποιούνται εξ ορισμού τιμές Όταν δημιουργείται ένα νήμα εκτελεί μία συνάρτηση νήματος που έχει τον κώδικα που πρέπει να εκτελέσει το νήμα – η συνάρτηση καθορίζεται μέσω της 3ης παραμέτρου μία κανονική συνάρτηση με ακριβώς μία παράμετρο τύπου void * και τύπο επιστροφής void * Η τιμή της παραμέτρου της συνάρτησης που εκτελείται παρέχεται μέσω του 4ου ορίσματος H pthread_create επιστρέφει αμέσως και συνεχίζει την εκτέλεση των επόμενων εντολών στη συνάρτηση Το γονικό και το θυγατρικό νήμα χρονοδρομολογούνται ασύγχρονα, χωρίς εγγύηση για το ποιο θα εκτελεστεί πρώτο
Δημιουργία νημάτων – παράδειγμα [pthread1.c] #include <pthread.h> #include <stdio.h> /* Τυπώνει 'x' στην έξοδο σφαλμάτων. Η παράμετρος δεν χρησιμοποιείται. Η συνάρτηση δεν επιστρέφει. */ void* print_xs(void* unused) { while (1) fputc ('x', stderr); return NULL; } /* Το κυρίως πρόγραμμα. */ int main () { pthread_t thread_id; /* Δημιουργία ενός νήματος που εκτελεί τη συνάρτηση print_xs. */ pthread_create(&thread_id, NULL, print_xs, NULL); /* Συνεχής εκτύπωση 'o' στην έξοδο σφαλμάτων */ fputc ('o', stderr); return 0;
Τερματισμός νημάτων Ένα νήμα τερματίζει όταν είτε: η συνάρτηση του νήματος ολοκληρωθεί (φθάνοντας στο τέλος της ή εκτελώντας την εντολή return) όταν στη ροή των εντολών κληθεί η συνάρτηση pthread_exit() void pthread_exit(void *); Το όρισμα στην εντολή return ή η παράμετρος στη συνάρτηση pthread_exit είναι η τιμή που επιστρέφεται από το νήμα
Μεταβίβαση παραμέτρων σε νήματα Η συνάρτηση που εκτελεί ένα νήμα δέχεται μία παράμετρο τύπου void * Η ίδια η παράμετρος δεν προσφέρεται για αποθήκευση πολλών δεδομένων μπορεί όμως να δείχνει σε έναν χώρο που περιέχει τιμές για τις παραμέτρους (χώρος: πίνακας, δομή, ένας αριθμός κ.λπ.) Συνηθισμένη πρακτική: Ορίζουμε έναν τύπο δομών για κάθε συνάρτηση που θέλουμε να εκτελούμε ως νήμα Για να καλέσουμε ένα νήμα, «γεμίζουμε» μία κατάλληλη δομή και μεταβιβάζουμε τη διεύθυνσή της στο νήμα
Μεταβίβαση παραμέτρων σε νήματα [pthread2.c] #include <pthread.h> #include <stdio.h> struct char_print_parms { /* Παράμετροι στη συνάρτηση εκτύπωσης */ char character; /* Ο χαρακτήρας προς εκτύπωση. */ int count; /* Το πλήθος των φορών που θα τυπωθεί. */ }; void* char_print (void* parameters) { /* Τυπώνει ένα πλήθος χαρακτήρων στην έξοδο σφαλμάτων, όπως ορίζει η παράμετρος parameters */ /* Μετατροπή δείκτη στον σωστό τύπο */ struct char_print_parms* p = (struct char_print_parms*) parameters; int i; for (i = 0; i < p->count; ++i) fputc (p->character, stderr); return NULL; } int main () { pthread_t thread1_id, thread2_id; struct char_print_parms thread1_args, thread2_args; /* Δημιουργία νήματος για τύπωμα 3.000 x. */ thread1_args.character = 'x'; thread1_args.count = 3000; pthread_create (&thread1_id, NULL, &char_print, &thread1_args); /* Δημιουργία νήματος για τύπωμα 2.000 o. */ thread2_args.character = 'o'; thread2_args.count = 2000; pthread_create (&thread2_id, NULL, &char_print, &thread2_args); /* Αναμονή (όχι συνιστώμενη μέθοδος) για ολοκλήρωση των νημάτων - ΓΙΑΤΙ; */ sleep(5); return 0; }
ΠΡΟΣΟΧΗ στην αλλαγή των δομών που αποτέλεσαν παραμέτρους σε νήματα [pthread2_2.c] #include <pthread.h> #include <stdio.h> struct char_print_parms { /* Παράμετροι στη συνάρτηση εκτύπωσης */ char character; int count; }; void* char_print (void* parameters) { /* Τυπώνει ένα πλήθος χαρακτήρων στην έξοδο σφαλμάτων, όπως ορίζει η παράμετρος parameters */ /* Μετατροπή δείκτη στον σωστό τύπο */ struct char_print_parms* p = (struct char_print_parms*) parameters; int i; for (i = 0; i < p->count; ++i) fputc (p->character, stderr); return NULL; } int main () { pthread_t thread1_id, thread2_id; struct char_print_parms thread_args; /* Δημιουργία νήματος για τύπωμα 3.000 x. */ thread_args.character = 'x'; thread_args.count = 3000; pthread_create (&thread1_id, NULL, &char_print, &thread_args); /* Δημιουργία νήματος για τύπωμα 2,000 o. */ thread_args.character = 'o'; thread_args.count = 2000; pthread_create (&thread2_id, NULL, &char_print, &thread_args); sleep(5); return 0; }
Αναμονή για νήματα Ο συνιστώμενος τρόπος αναμονής για την ολοκλήρωση εκτέλεσης των νημάτων είναι η pthread_join int pthread_join (pthread_t threadId, void **returnValue); Η πρώτη παράμετρος καθορίζει την ταυτότητα του νήματος που θέλουμε να περιμένουμε Δεν υπάρχει ειδική τιμή που να περιμένει για οποιοδήποτε νήμα Η δεύτερη παράμετρος υποδεικνύει το σημείο όπου θα τοποθετηθεί ένας δείκτης προς το αποτέλεσμα που επιστρέφει η συνάρτηση του νήματος Αν ο δείκτης είναι NULL το αποτέλεσμα δεν επιστρέφεται, απλά αναμένεται το νήμα Η pthread_join επιστρέφει 0 για επιτυχία ή μη μηδενικό αποτέλεσμα για αποτυχία Το (μη μηδενικό) αποτέλεσμα της pthread_join μπορεί να χρησιμοποιηθεί ως παράμετρος στην strerr για να εξαχθεί το σχετικό μήνυμα σφάλματος – ΔΕΝ αλλάζει το errno (γιατί;)
Αναμονή για νήματα [pthread3.c] #include <pthread.h> #include <stdio.h> struct char_print_parms { /* Παράμετροι στη συνάρτηση εκτύπωσης */ char character; int count; }; void* char_print (void* parameters) { /* Τυπώνει ένα πλήθος χαρακτήρων στην έξοδο σφαλμάτων, όπως ορίζει η παράμετρος parameters */ /* Μετατροπή δείκτη στον σωστό τύπο */ struct char_print_parms* p = (struct char_print_parms*) parameters; int i; for (i = 0; i < p->count; ++i) fputc (p->character, stderr); return NULL; } int main () { pthread_t thread1_id, thread2_id; struct char_print_parms thread1_args, thread2_args; /* Δημιουργία νήματος για τύπωμα 3.000 x. */ thread1_args.character = 'x'; thread1_args.count = 3000; pthread_create (&thread1_id, NULL, &char_print, &thread1_args); /* Δημιουργία νήματος για τύπωμα 2.000 o. */ thread2_args.character = 'o'; thread2_args.count = 2000; pthread_create (&thread2_id, NULL, &char_print, &thread2_args); if ((status = pthread_join(thread1_id, NULL)) != 0) fprintf(stderr, "joining t1: %s\n", strerror(status)); if ((status = pthread_join(thread2_id, NULL)) != 0) fprintf(stderr, "joining t2: %s\n", strerror(status)); return 0; }
Επιστροφή τιμών από τα νήματα Αν η δεύτερη παράμετρος προς την pthread_join δεν έχει την τιμή NULL, σ’ αυτή τοποθετείται ένας δείκτης προς το αποτέλεσμα που έχει επιστρέψει το συγκεκριμένο νήμα ΠΡΟΣΟΧΗ: η μνήμη προς την οποία δείχνει το αποτέλεσμα πρέπει να εξακολουθεί να είναι έγκυρη ΠΟΤΕ δεν επιστρέφουμε τη διεύθυνση μιας τοπικής μεταβλητής Μπορούμε να δεσμεύσουμε μνήμη με malloc και να τοποθετήσουμε εκεί το αποτέλεσμα Ο κώδικας που ξεκινά το νήμα μπορεί να υποδείξει τη θέση όπου θα τοποθετηθεί το αποτέλεσμα
Επιστροφή τιμών από τα νήματα [pthread_res1.c] void *compute_sum(void* parameters) { /* Υπολογίζει το άθροισμα 1..ν όπου το ν δίνεται από την παράμετρο */ int limit = *(int *)parameters, i; long result, *res; for (i = 0, result = 0; i <= limit; ++i) result += i; if ((res = (void *)malloc(sizeof(long))) == NULL) return NULL; *res = result; return res; } int main (void) { pthread_t thread1_id, thread2_id; int status, lim1, lim2; long *res1, *res2; printf("Δώστε τα δύο όρια: "); scanf("%d %d", &lim1, &lim2); /* Δημιουργία νημάτων για υπολογισμό */ pthread_create (&thread1_id, NULL, compute_sum, (void *)&lim1); pthread_create (&thread2_id, NULL, compute_sum, (void *)&lim2); if ((status = pthread_join(thread1_id, (void *)&res1)) != 0) fprintf(stderr, "joining t1: %s\n", strerror(status)); if ((status = pthread_join(thread2_id, (void *)&res2)) != 0) fprintf(stderr, "joining t2: %s\n", strerror(status)); printf("Sum [1..%d] = %ld\nSum [1..%d] = %ld\n", lim1, (res1 == NULL) ? -1L : *res1, lim2, (res2 == NULL) ? -1L : *res2); return 0;
Επιστροφή τιμών από τα νήματα [pthread_res2.c] struct limit_param {int limit; long *res_location;}; void *compute_sum(void* parameters) { /* Υπολογίζει το άθροισμα 1..ν, όπου το ν δίνεται από την παράμετρο */ int i; long res; struct limit_param *params = (struct limit_param *)parameters; for (i = 0, res = 0; i <= params->limit; ++i) res += i; *(params->res_location) = res; return NULL; } int main () { pthread_t thread1_id, thread2_id; int status; struct limit_param lp1, lp2; long res1, res2; printf("Δώστε τα δύο όρια: "); scanf("%d %d", &lp1.limit, &lp2.limit); lp1.res_location = &res1; lp2.res_location = &res2; /* Δημιουργία νημάτων για υπολογισμό . */ pthread_create (&thread1_id, NULL, compute_sum, (void *)&lp1); pthread_create (&thread2_id, NULL, compute_sum, (void *)&lp2); if ((status = pthread_join(thread1_id, NULL)) != 0) fprintf(stderr, "joining t1: %s\n", strerror(status)); if ((status = pthread_join(thread2_id, NULL)) != 0) fprintf(stderr, "joining t2: %s\n", strerror(status)); printf("Sum [1..%d] = %ld\nSum [1..%d] = %ld\n", lp1.limit, res1, lp2.limit, res2); return 0; }
Ταυτότητες νημάτων Κάθε νήμα έχει μία μοναδική ταυτότητα στα πλαίσια της διεργασίας Ο δημιουργός του νήματος μαθαίνει την ταυτότητα από την 1η παράμετρο της pthread_create Το ίδιο το νήμα μπορεί να πληροφορηθεί την ταυτότητά του με την pthread_self() Δύο ταυτότητες νημάτων συγκρίνονται με την pthread_equal int pthread_equal(pthread_t tid1, pthread_t tid2); Η ταυτότητα μπορεί να χρησιμοποιηθεί στην pthread_join, στην pthread_cancel, στην pthread_detach κ.λπ.
Αποσπασμένα νήματα Εξ ορισμού ένα νήμα επιστρέφει κάποιο αποτέλεσμα και το γονικό του (ή άλλο) νήμα μπορεί (και πρέπει) να περιμένει για την ολοκλήρωσή του αλλιώς το νήμα γίνεται το αντίστοιχο της «διεργασίας-ζόμπι» Έχουμε τη δυνατότητα να δημιουργήσουμε νήματα που δεν επιστρέφουν αποτελέσματα και δεν χρειάζεται να περιμένουμε για την ολοκλήρωσή τους τα νήματα αυτά καλούνται αποσπασμένα νήματα (detached threads) τα αποτελέσματα που επιστρέφουν με τη return ή την pthread_exit αγνοούνται προσπάθειες να περιμένουμε για την ολοκλήρωσή τους με την pthread_join αποτυγχάνουν Ένα μη αποσπασμένο νήμα μπορεί να αποσπαστεί με την pthread_detach(pthread_t tid); - η αντίστροφη αλλαγή κατάστασης δεν είναι εφικτή
Αποσπασμένα νήματα [pthread_det.c] #include <pthread.h> #include <stdio.h> void* beeper (void* parameters) { while(1) { sleep(1); putchar('\a'); fflush(stdout); } int main () { pthread_t thread1_id; char s[128]; /* Δημιουργία νήματος για ηχητική επένδυση */ pthread_create (&thread1_id, NULL, &beeper, NULL); pthread_detach(thread1_id); printf("Enter password: "); gets(s); return 0; }
Τερματισμός νημάτων Κανονικά ένα νήμα τελειώνει όταν ολοκληρώνεται η συνάρτηση του νήματος καλεί την pthread_exit() Είναι δυνατόν να ζητήσουμε τον τερματισμό ενός νήματος μέσω της pthread_cancel Αν ένα νήμα που τερματίζεται δεν είναι αποσπασμένο, τότε πρέπει να ζητήσουμε την κατάστασή του μέσω της pthread_join η τιμή επιστροφής ενός τερματισμένου νήματος είναι η PTHREAD_CANCELED
Τερματισμός νημάτων [pthread_cancel.c] void* beeper (void* parameters) { while(1) { sleep(2); putchar('\a'); fflush(stdout); } int main () { pthread_t thread1_id; char s[128]; while (1) { /* Δημιουργία νήματος για ηχητική επένδυση */ pthread_create (&thread1_id, NULL, &beeper, NULL); pthread_detach(thread1_id); printf("Enter password: "); gets(s); pthread_cancel(thread1_id); if (strcmp(s, "secret") == 0) break; printf("Wrong password!\n"); printf("Format drive [y/n]?"); gets(s); return 0; }
Τερματισμός νημάτων [pthread_cancel2.c] void *compute_mean_rnd(void* parameters) { /* Υπολογίζει τον μέσο τυχαίο αριθμό από LONG_MAX * LONG_MAX δείγματα */ long i, j; double result, *res; for (i = 0, result = 0; i < LONG_MAX; ++i) for (j = 0; j < LONG_MAX; ++j) result += rand(); if ((res = (void *)malloc(sizeof(double))) == NULL) return NULL; *res = result / LONG_MAX / LONG_MAX; return res; } void *brutus(void *parameters) { pthread_detach(pthread_self()); sleep(10); pthread_cancel(*(pthread_t *)parameters); int main () { pthread_t thread1_id, thread2_id; int status; double *res1; /* Δημιουργία νήματος για υπολογισμό . */ pthread_create(&thread1_id, NULL, compute_mean_rnd, NULL); pthread_create(&thread2_id, NULL, brutus, &thread1_id); if ((status = pthread_join(thread1_id, (void *)&res1)) != 0) fprintf(stderr, "joining t1: %s\n", strerror(status)); if (res1 == PTHREAD_CANCELED) fprintf(stderr, "Computation thread canceled;"); else printf("Mean random = %lf\n", (res1 == NULL) ? -1.0 : *res1); return 0;
Τερματισμός νημάτων Τα νήματα δεν τερματίζονται πάντα οπουδήποτε (ή καθόλου) εξαρτάται από την πολιτική τερματισμού του κάθε νήματος Δυνατές πολιτικές: δυνατότητα ασύγχρονου τερματισμού (asynchronously cancelable): Σε οποιοδήποτε σημείο της εκτέλεσής του δυνατότητα σύγχρονου τερματισμού (synchronously cancelable): σε «επιλεγμένα σημεία» της εκτέλεσής του (είσοδος/έξοδος, αναμονή, ρητός έλεγχος τερματισμού με την pthread_testcancel) Χωρίς δυνατότητα τερματισμού (uncancelable) Ορισμός πολιτικής: int pthread_setcanceltype(int newType, int *oldType); /* type = PTHREAD_CANCEL_ASYNCHRONOUS, PTHREAD_CANCEL_DEFERRED */ int pthread_setcancelstate(int newState, int *oldState); /* state = PTHREAD_CANCEL_DISABLE, PTHREAD_CANCEL_ENABLE */
Τερματισμός νημάτων [pthread_cancel3.c] void *compute_mean_rnd(void* parameters) { long i, j; double result, *res; pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); for (i = 0, result = 0; i < LONG_MAX; ++i) for (j = 0; j < LONG_MAX; ++j) result += rand(); if ((res = (void *)malloc(sizeof(double))) == NULL) return NULL; *res = result / LONG_MAX / LONG_MAX; return res; } void *brutus(void *parameters) { pthread_detach(pthread_self()); sleep(10); pthread_cancel(*(pthread_t *)parameters); int main () { pthread_t thread1_id, thread2_id; int status; double *res1; /* Δημιουργία νήματος για υπολογισμό */ pthread_create(&thread1_id, NULL, compute_mean_rnd, NULL); pthread_create(&thread2_id, NULL, brutus, &thread1_id); if ((status = pthread_join(thread1_id, (void *)&res1)) != 0) fprintf(stderr, "joining t1: %s\n", strerror(status)); if (res1 == PTHREAD_CANCELED) fprintf(stderr, "Computation thread canceled"); else printf("Mean random = %lf\n", (res1 == NULL) ? -1.0 : *res1); return 0;
Τερματισμός νημάτων /* Νήμα για μεταφορά ποσού από έναν τραπεζικό λογαριασμό σε έναν άλλο. Το τμήμα της μεταφοράς πρέπει να εκτελεστεί εξ ολοκλήρου ή καθόλου */ /* έλεγχος στοιχείων – εδώ μπορεί να διακοπεί */ ... /* Κώδικας για τη μεταφορά */ /* 1. απενεργοποίηση της δυνατότητας τερματισμού */ setcancelstate(PTHREAD_CANCEL_DISABLE, &oldCancelState); /* 2. πραγματοποίηση της μεταφοράς */ lseek(accountFile, account1Offset, SEEK_SET); read(accountFile, &account1, sizeof(accountType)); account1.balance -= amount; write(accountFile, &account1, sizeof(accountType)); lseek(accountFile, account2Offset, SEEK_SET); read(accountFile, &account2, sizeof(accountType)); account1.balance += amount; write(accountFile, &account2, sizeof(accountType)); /* Επαναφορά της πρότερης κατάστασης για τη δυνατότητα τερματισμού */ setcancelstate(oldCancelState, NULL);
Χαρακτηριστικά νημάτων Μέσω της δεύτερης παραμέτρου στην pthread_create ορίζονται χαρακτηριστικά του νήματος που θα δημιουργηθεί Τα χαρακτηριστικά ενσωματώνονται σε ένα αντικείμενο τύπου pthread_attr_t και αφορούν: κατάσταση απόσπασης (detach state) πολιτική και παραμέτρους χρονοπρογραμματισμού μέγεθος, διεύθυνση και μέγεθος «χώρου ελέγχου» στοίβας Χρήση αντικειμένων pthread_attr_t: δήλωση Αρχικοποίηση σε pthread_attr_init Ορισμός τιμών χαρακτηριστικών (όχι απαραίτητα όλων) με χρήση εξειδικευμένων συναρτήσεων (στη συνέχεια) Παράθεση του αντικειμένου ως δεύτερο όρισμα στην pthread_create Καταστροφή του αντικειμένου μέσω της pthread_attr_destroy Ένα σύνολο χαρακτηριστικών μπορεί να αποτελέσει πρότυπο πολλών νημάτων Είναι αδιάφορο τι θα κάνουμε με το σύνολο χαρακτηριστικών μετά τη δημιουργία του νήματος
Χαρακτηριστικά νημάτων και κατάσταση απόσπασης Δυνατότητα δημιουργίας νημάτων που είναι εξ αρχής αποσπασμένα int pthread_attr_setdetachstate(pthread_attr_t *tattr,int detachstate); /* Επιστρέφει 0 σε επιτυχία */ int pthread_attr_getdetachstate(const pthread_attr_t *tattr, int *detachstate); /* τιμές για detachState: PTHREAD_CREATE_JOINABLE, PTHREAD_CREATE_DETACHED – αν η getdetachstate επιστρέψει οτιδήποτε άλλο, είναι ένδειξη αποτυχίας */ Η εξ ορισμού τιμή (μετά την pthread_attr_init) είναι PTHREAD_CREATE_JOINABLE
Δημιουργία αποσπασμένων νημάτων [pthread_attr1.c] void* beeper (void* parameters) { while(1) { sleep(1); putchar('\a'); fflush(stdout); } int main () { pthread_t thread1_id; pthread_attr_t theAttrs; char s[128]; int test; /* Ορισμός χαρακτηριστικών */ pthread_attr_init(&theAttrs); pthread_attr_setdetachstate(&theAttrs, PTHREAD_CREATE_DETACHED); if (pthread_attr_getdetachstate(&theAttrs, &test) != 0) puts("unable to get state from template"); else if (test == PTHREAD_CREATE_DETACHED) puts("template will create detached processes"); else puts("template will create joinable processes"); /* Δημιουργία νήματος για ηχητική επένδυση */ pthread_create (&thread1_id, &theAttrs, &beeper, NULL); printf("Enter password: "); gets(s); return 0;
Χρονοπρογραμματισμός νημάτων Κάθε νήμα εντάσσεται σε μία από τέσσερις πολιτικές χρονοπρογραμματισμού: SCHED_FIFO: Πάντα εκτελείται το νήμα με την υψηλότερη προτεραιότητα που δεν έχει ανασταλεί. Αν υπάρχουν πολλά νήματα με την ίδια προτεραιότητα, εκτελείται ένα από αυτά μέχρι να ανασταλεί. SCHED_RR: Πάντα εκτελείται το νήμα με την υψηλότερη προτεραιότητα που δεν έχει ανασταλεί. Αν υπάρχουν πολλά νήματα με την ίδια προτεραιότητα, εκτελούνται εκ περιτροπής για κάποιο χρονομερίδιο το καθένα SCHED_OTHER (εξ ορισμού επιλογή): Προγραμματισμός χρονομεριδίων – όλα τα νήματα θα λάβουν ένα χρονομερίδιο ασχέτως με το αν υπάρχουν νήματα υψηλότερης προτεραιότητας που δεν έχουν ανασταλεί. Τα νήματα υψηλότερης προτεραιότητας μπορεί να λάβουν μεγαλύτερο χρονομερίδιο
Χρονοπρογραμματισμός νημάτων Η προτεραιότητα του κάθε νήματος ορίζεται ως παράμετρος χρονοπρογραμματισμού Όταν ξεκινάει ένα νήμα μπορεί να κληρονομεί τις παραμέτρους χρονοπρογραμματισμού του γονέα του (εξ ορισμού) ή να χρησιμοποιεί τις ρητά ορισμένες σε ένα πρότυπο Συναρτήσεις για απ’ ευθείας ορισμό: int pthread_getschedparam (pthread_t threadId, int *policy, struct sched_param *priority); int pthread_setschedparam (pthread_t threadId, int policy, const struct sched_param *priority); struct sched_param { ... int sched_priority; ...}; Πολιτική: SCHED_FIFO, SCHED_RR, SCHED_OTHER Προτεραιότητα: ανάλογα με την πολιτική P μεταξύ sched_get_priority_min(P) και sched_get_priority_max(P)
Χρονοπρογραμματισμός νημάτων [pthread_sched1.c] #include <pthread.h> #include <stdio.h> /* Συνεχής εκτύπωση 'x' στην έξοδο σφαλμάτων */ void* print_xs(void* unused) { while (1) fputc ('x', stderr); return NULL; } int main (int argc, char *argv[]) { pthread_t thread_id; struct sched_param s1; pthread_create(&thread_id, NULL, print_xs, NULL); s1.sched_priority = sched_get_priority_min(SCHED_FIFO); pthread_setschedparam(thread_id, SCHED_FIFO, &s1); s1.sched_priority = sched_get_priority_max(SCHED_FIFO); pthread_setschedparam(pthread_self(), SCHED_FIFO, &s1); /* Συνεχής εκτύπωση 'o' στην έξοδο σφαλμάτων*/ fputc ('o', stderr); return 0;
Χρονοπρογραμματισμός νημάτων Συναρτήσεις για χειρισμό μέσω προτύπων: int pthread_attr_getinheritsched (const pthread_attr_t *, int *); int pthread_attr_setinheritsched (pthread_attr_t *, int); Ορισμός ένδειξης κληροδότησης παραμέτρων χρονοπρογραμματισμού: PTHREAD_EXPLICIT_SCHED, PTHREAD_INHERIT_SCHED int pthread_attr_getschedparam (const pthread_attr_t *, struct sched_param *); int pthread_attr_getschedpolicy (const pthread_attr_t *, int *); int pthread_attr_setschedparam (pthread_attr_t *, const struct sched_param *); int pthread_attr_setschedpolicy (pthread_attr_t *, int);
Χρονοπρογραμματισμός νημάτων [pthread_sched_attr.c] #include <pthread.h> #include <stdio.h> void* print_xs(void* unused) { while (1) fputc ('x', stderr); return NULL; } int main (int argc, char *argv[]) { pthread_t thread_id; pthread_attr_t theAttrs; struct sched_param s1; pthread_attr_init(&theAttrs); pthread_attr_setinheritsched(&theAttrs, PTHREAD_EXPLICIT_SCHED); pthread_attr_setschedpolicy(&theAttrs, SCHED_OTHER); s1.sched_priority = sched_get_priority_max(SCHED_OTHER); pthread_attr_setschedparam(&theAttrs, &s1); /* Δημιουργία νήματος με τους παραμέτρους χρονοπρογραμματισμού */ pthread_create(&thread_id, &theAttrs, print_xs, NULL); fputc ('o', stderr); return 0;
Καθολικές μεταβλητές νημάτων Σε ένα πρόγραμμα υπάρχουν καθολικές μεταβλητές που ισχύουν σε όλο το πρόγραμμα τοπικές μεταβλητές που ισχύουν σε ένα τμήμα κώδικα Με την εισαγωγή των νημάτων χρειαζόμαστε μία τρίτη κατηγορία εμβέλειας: μεταβλητές που ισχύουν καθολικά σε επίπεδο νήματος δηλ. είναι ορατές από όλες τις διαδικασίες που εκτελούνται σε ένα νήμα (για ανάγνωση και τροποποίηση τιμής) δεν είναι ορατές από κανένα άλλο νήμα Οι γλώσσες προγραμματισμού δεν διαθέτουν συντακτικές δομές για δήλωση μεταβλητών με αυτό το επίπεδο εμβέλειας διαχείριση των μεταβλητών μέσω συναρτήσεων βιβλιοθήκης
Διαχείριση καθολικών μεταβλητών νημάτων Δημιουργία της μεταβλητής (thread-specific key) μέσω της pthread_key_create ισοδύναμο με τη δήλωση της μεταβλητής Κάθε νήμα χρησιμοποιεί την pthread_setspecific για να αποδώσει τιμή στη μεταβλητή (η τιμή ισχύει για το συγκεκριμένο νήμα) και την pthread_getspecific για να διαβάσει την τρέχουσα τιμή της μεταβλητής Οι τιμές στις pthread_setspecific και pthread_getspecific είναι τύπου void *, προκειμένου να είναι δυνατή η αποθήκευση οποιουδήποτε δεδομένου το νήμα πρέπει να κάνει τις κατάλληλες μετατροπές τύπων για δεδομένα άλλων τύπων
Διαχείριση καθολικών μεταβλητών νημάτων – Παράδειγμα [pthread_glocal Διαχείριση καθολικών μεταβλητών νημάτων – Παράδειγμα [pthread_glocal.c 1/2] pthread_key_t thread_result_file; pthread_key_t thread_ordinal; void close_result_file(void *theFile) { fclose((FILE *)theFile); } /* Main program. */ int main () { pthread_t thread1_id, thread2_id; pthread_key_create(&thread_result_file, close_result_file); pthread_key_create(&thread_ordinal, NULL); pthread_create(&thread1_id, NULL, worker_init, (void *)1); pthread_create(&thread2_id, NULL, worker_init, (void *)2); pthread_join(thread1_id, NULL); pthread_join(thread2_id, NULL); return 0;
Διαχείριση καθολικών μεταβλητών νημάτων – Παράδειγμα [pthread_glocal Διαχείριση καθολικών μεταβλητών νημάτων – Παράδειγμα [pthread_glocal.c 1/2] void worker() { FILE *theFile; int theId; theId = *(int *)(pthread_getspecific(thread_ordinal)); theFile = (FILE *)(pthread_getspecific(thread_result_file)); fprintf(theFile, "thread %d: bored to do any work\n", theId); } void *worker_init(void* parameters) { FILE *myOutput; char myFileName[128]; int myId; myId = (int)(parameters); pthread_setspecific(thread_ordinal, &myId); sprintf(myFileName, "%s%d", "result", myId); myOutput = fopen(myFileName, "w"); pthread_setspecific(thread_result_file, myOutput); worker(); return NULL;
Συναρτήσεις καθαρισμού περιβάλλοντος νημάτων Έστω ο κώδικας void do_some_work () { /* Δέσμευση μνήμης */ void* temp_buffer = allocate_buffer (1024); /* Εργασία κατά τη διάρκεια της οποίας μπορεί να κληθεί η pthread_exit ή να τερματιστεί το νήμα */ free(temp_buffer); } Τι συμβαίνει με το χώρο temp_buffer αν το νήμα καλέσει την pthread_exit ή θανατωθεί ενόσω βρίσκεται μεταξύ του allocate_buffer και του free; Η μνήμη μένει δεσμευμένη (διαρροή μνήμης) Το ίδιο μπορεί να συμβεί και με άλλους πόρους (π.χ. αρχεία, σημαφόρους κ.λπ.)
Συναρτήσεις καθαρισμού περιβάλλοντος νημάτων Οι καθολικές μεταβλητές νημάτων δίνουν μία λύση (μέσω της συνάρτησης που ορίζεται στην pthread_key_create), αλλά είναι απαραίτητο να ορίσουμε μία μεταβλητή για κάθε πόρο Η βιβλιοθήκη νημάτων παρέχει δύο ειδικές συναρτήσεις για τη διευκόλυνση του καθαρισμού περιβάλλοντος νημάτων: void pthread_cleanup_push(void (*routine) (void *), void *arg); void pthread_cleanup_pop(int execute); pthread_cleanup_push: ορίζει ότι στον τερματισμό (ή θανάτωση) ενός νήματος θα εκτελεστεί η διαδικασία routine με παράμετρο arg pthread_cleanup_pop: αφαιρεί την πιο πρόσφατα ορισμένη συνάρτηση καθαρισμού περιβάλλοντος του νήματος Αν η παράμετρος έχει τιμή 1, η συνάρτηση εκτελείται Είναι απαραίτητο για κάθε pthread_cleanup_push να υπάρχει αντίστοιχη pthread_cleanup_pop στην ίδια συνάρτηση και στο ίδιο επίπεδο ένθεσης
Συναρτήσεις καθαρισμού περιβάλλοντος νημάτων #include <malloc.h> #include <pthread.h> void* allocate_buffer (size_t size) { /* Δέσμευση μνήμης */ return malloc (size); } void deallocate_buffer (void* buffer) { /* Αποδέσμευση μνήμης */ free (buffer); void do_some_work () { void* temp_buffer = allocate_buffer (1024); /* Δεσμεύουμε μνήμη */ /* Ορίζουμε συνάρτηση καθαρισμού για τη μνήμη, για την περίπτωση που το νήμα καλέσει την pthread_exit ή θανατωθεί */ pthread_cleanup_push(deallocate_buffer, temp_buffer); /* Εργασία κατά τη διάρκεια της οποίας μπορεί να κληθεί η pthread_exit ή να τερματιστεί το νήμα */ /* Ανάκληση του ορισμού της συνάρτησης καθαρισμού. Μια και δίνουμε παράμετρο ίση με 1, η συνάρτηση εκτελείται */ pthread_cleanup_pop(1);
Συγχρονισμός και κρίσιμα τμήματα Τα νήματα εισάγουν ανάγκες συγχρονισμού μια και διαφορετικά νήματα μπορεί να προσπελαύνουν τα ίδια δεδομένα δεν υπάρχει εγγύηση ποιο νήμα θα εκτελεστεί πρώτο αν υπάρχουν πολλαπλοί επεξεργαστές μπορεί νήματα να εκτελούνται παράλληλα Η μη συντονισμένη πρόσβαση στα ίδια δεδομένα εισάγει συνθήκες ανταγωνισμού (race conditions)
Παράδειγμα συνθήκης ανταγωνισμού struct job { /* ... διάφορα δεδομένα εργασίας */ struct job* next; /* Δείκτης στην επόμενη εγγραφή */ }; struct job* job_queue; /* Συνδεδεμένη λίστα εργασιών */ void *thread_function (void *arg) { while (job_queue != NULL) { /* Εκτέλεση εργασιών όσο η ουρά έχει */ struct job* next_job = job_queue; /* Η επόμενη εργασία */ job_queue = job_queue->next; /* Αφαίρεση εργασίας από την ουρά */ process_job (next_job); /* Εκτέλεση της εργασίας */ free (next_job); /* Αποδέσμευση μνήμης */ } return NULL; Έστω δύο νήματα που εκτελούν αυτόν τον κώδικα Γκαντεμιά: το 1ο νήμα σταματάει έχοντας εκτελέσει τη σημειωμένη γραμμή και το νήμα 2 τελειώνει την τρέχουσα εργασία και πάει να διαβάσει την επόμενη Η εργασία που βρισκόταν 1η θα εκτελεστεί δύο φορές και η επόμενη καμία! Αν δεν υπάρχει επόμενη, θα έχουμε πρόσβαση μέσω δείκτη NULL (SIGSEGV) Αυτό συνέβη διότι η πρόσβαση στην job_queue είναι ανεξέλεγκτη!
pthread_mutex: Αμοιβαίος αποκλεισμός Λύση: αφήνουμε ένα μόνο νήμα να προσπελαύνει την κρίσιμη μεταβλητή ανά πάσα στιγμή Για κάθε πόρο που πρέπει να προσπελαύνεται μόνο από ένα νήμα τη φορά ορίζουμε (και αρχικοποιούμε) μία μεταβλητή τύπου pthread_mutex_t Δήλωση : pthread_mutex_t mymutex; Αρχικοποίηση: pthread_mutex_init (&mymutex, &attr); pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER; Πριν προσπελάσει τον πόρο, το κάθε νήμα πρέπει να καλεί την pthread_mutex_lock για τη σχετική μεταβλητή Όταν ολοκληρώσει την προσπέλαση του πόρου το νήμα καλεί την pthread_mutex_unlock για τη σχετική μεταβλητή, προκειμένου να επιτρέψει σε άλλα νήματα την προσπέλαση Το σύστημα εγγυάται ότι το πολύ ένα νήμα θα εκτελεί κώδικα που βρίσκεται ανάμεσα σε pthread_mutex_lock και pthread_mutex_unlock που αφορούν την ίδια μεταβλητή – αλλιώς: αν ένα νήμα καλέσει την pthread_mutex_lock και ο mutex δεν είναι κλειδωμένος, τότε ο mutex κλειδώνει και το νήμα συνεχίζει αλλιώς το νήμα περιμένει μέχρι να κληθεί η pthread_mutex_unlock
pthread_mutex: Αμοιβαίος αποκλεισμός pthread_mutex_t job_queue_mutex = PTHREAD_MUTEX_INITIALIZER; void* thread_function (void* arg) { while (1) { /* Το while αλλάζει δομή για να χρησιμοποιηθεί ο mutex */ struct job* next_job; /* κλείδωμα της πρόσβασης στην ουρά */ pthread_mutex_lock (&job_queue_mutex); /* ασφαλής (πλέον) έλεγχος και τροποποίηση */ if (job_queue == NULL) next_job = NULL; else { /* Εξαγωγή της επόμενης διαθέσιμης εργασίας */ next_job = job_queue; /* και απομάκρυνσή της από την ουρά */ job_queue = job_queue->next; } pthread_mutex_unlock(&job_queue_mutex); /* Άρση του κλειδώματος */ /* Αν η ουρά ήταν κενή, τερματισμός του νήματος */ if (next_job == NULL) break; process_job(next_job); /* Εκτέλεση της εργασίας */ free(next_job); /* καθαρισμός περιβάλλοντος */ return NULL;
pthread_mutex: Αμοιβαίος αποκλεισμός Προσοχή Στο παράδειγμα ο βρόχος τερματίζεται μόνον αφού ξεκλειδωθεί ο σχετικός mutex – αλλιώς o mutex θα παραμείνει κλειδωμένος και άλλα νήματα θα περιμένουν επ’ αόριστον Ο mutex πρέπει να χρησιμοποιείται και από άλλα τμήματα κώδικα που προσπελαύνουν τη μεταβλητή π.χ. void enqueue_job (struct job* new_job) { pthread_mutex_lock (&job_queue_mutex); new_job->next = job_queue; job_queue = new_job; pthread_mutex_unlock (&job_queue_mutex); }
Mutexes και αδιέξοδα Με τα mutexes ένα νήμα μπορεί να αναστείλει κάποιο άλλο Αν οι αναστολές δημιουργήσουν κύκλο έχουμε αδιέξοδο Ν1 περιμένει Ν2 περιμένει … περιμένει Νκ περιμένει Ν1 Μπορούν να δημιουργηθούν ακόμη και αδιέξοδα με ένα νήμα αν κλειδώσει δύο φορές έναν mutex Π.χ. εξαγωγή της εργασίας με τη μεγαλύτερη προτεραιότητα: pthread_mutex_lock(&job_queue_mutex); theJob = find_max_priority(theQueue); remove_item(&theQueue, theJob); pthread_mutex_unlock(&job_queue_mutex); struct job *find_max_priority(struct job *theQueue) { ... return result; }
Mutexes και αδιέξοδα Ειδικότερα για τα αδιέξοδα με ένα μόνο νήμα, παρέχονται οι εξής επιλογές για τους mutexes: Γρήγοροι mutexes: η εξ ορισμού συμπεριφορά – ένα νήμα μπορεί να δημιουργήσει αδιέξοδο με τον εαυτό του Αναδρομικοί mutexes: το νήμα που εκτέλεσε επιτυχές pthread_mutex_lock μπορεί να εκτελέσει οσαδήποτε pthread_mutex_lock χωρίς να ανασταλεί Πρέπει να εκτελέσει όμως ισάριθμα pthread_mutex_unlock Mutexes με έλεγχο σφαλμάτων: αν ένα νήμα εκτελέσει δεύτερο pthread_mutex_lock η κλήση αποτυγχάνει και το errno τίθεται σε EDEADLK Για ορισμό αναδρομικών mutexes και mutexes με έλεγχο σφαλμάτων πρέπει να χρησιμοποιηθεί η pthread_mutex_init με παράθεση χαρακτηριστικών (attributes)
Δημιουργία & χρήση αναδρομικών mutexes pthread_mutexattr_t attr; pthread_mutex_t job_queue_mutex; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&job_queue_mutex, &attr); pthread_mutexattr_destroy(&attr); pthread_mutex_lock(&job_queue_mutex); theJob = find_max_priority(theQueue); remove_item(&theQueue, theJob); pthread_mutex_unlock(&job_queue_mutex); struct job *find_max_priority(struct job *theQueue) { ... return result; }
Δημιουργία & χρήση mutexes με έλεγχο σφαλμάτων pthread_mutexattr_t attr; pthread_mutex_t job_queue_mutex; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); pthread_mutex_init(&job_queue_mutex, &attr); pthread_mutexattr_destroy(&attr); pthread_mutex_lock(&job_queue_mutex); theJob = find_max_priority(theQueue); remove_item(&theQueue, theJob); pthread_mutex_unlock(&job_queue_mutex); struct job *find_max_priority(struct job *theQueue) { int do_unlock = 1; if ((pthread_mutex_lock(&job_queue_mutex) == -1) && (errno == EDEADLK)) do_unlock = 0; ... if (do_unlock) pthread_mutex_unlock(&job_queue_mutex); return result; }
Παράδειγμα τύπων mutexes [mutex-types.c] #include <pthread.h> #include <stdio.h> #include <errno.h> pthread_mutex_t mutex; int main(int argc, char *argv[]) { pthread_mutexattr_t mta; int type = PTHREAD_MUTEX_DEFAULT, rc, i; if (argc > 1 && argv[1][0] == 'r') type = PTHREAD_MUTEX_RECURSIVE; else if (argc > 1 && argv[1][0] == 'e') type = PTHREAD_MUTEX_ERRORCHECK; rc = pthread_mutexattr_init(&mta); printf("Setting mutex type...\n"); rc = pthread_mutexattr_settype(&mta, type); printf("Initialising mutex...\n"); rc = pthread_mutex_init(&mutex, &mta); printf("Lock the mutex\n"); rc = pthread_mutex_lock(&mutex); printf("Re-Lock the mutex\n"); rc = pthread_mutex_lock(&mutex); if (rc != 0) printf("re-lock: %s\n", strerror(rc)); printf("Unlock the mutex 3 times\n"); for (i = 0; i < 3; i++) if ((rc = pthread_mutex_unlock(&mutex)) != 0) printf("Unlock: %s\n", strerror(rc)); printf("Cleanup\n"); rc = pthread_mutex_destroy(&mutex); return 0; }
Μη ανασταλτικός έλεγχος mutex int pthread_mutex_trylock(pthread_mutex_t *mutex); Αν μπορεί να κλειδώσει τον mutex τότε τον κλειδώνει και επιστρέφει 0 Αν δεν μπορεί να κλειδώσει τον mutex (διότι είναι κλειδωμένος) τότε επιστρέφει αμέσως την τιμή EBUSY Το αν μπορεί να κλειδωθεί ή όχι καθορίζεται όπως και στο κανονικό κλείδωμα (κανονικοί, αναδρομικοί, με έλεγχο σφάλματος mutexes)
Σημαφόροι για νήματα Οι mutexes παρέχουν αμοιβαίο αποκλεισμό για τα νήματα Δεν είναι πάντα επαρκές Π.χ. αν έχουμε ενδιάμεση μνήμη με Ν θέσεις ο έλεγχος για το αν υπάρχουν κενές θέσεις ή αντικείμενα στην ενδιάμεση μνήμη θα είναι εξαιρετικά αναποτελεσματικός pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int num_elements = 0; struct job *queue; void enqueue(struct job *newJob) { while (1) { pthread_mutex_lock(&mutex); if (num_elements != QUEUE_MAX) { num_elements++; newJob->next = queue; queue = newJob; pthread_mutex_unlock(&mutex); return; }
Σημαφόροι για νήματα Οι σημαφόροι νημάτων παρέχουν μέθοδο συγχρονισμού για τέτοιες περιπτώσεις Αρχείο ενσωμάτωσης: <semaphore.h> Δήλωση σημαφόρου: sem_t mysem; Αρχικοποίηση: int sem_init(sem_t *sem, int pshared, unsigned int value); pshared: 0 συγχρονισμός μεταξύ νημάτων της ίδιας διεργασίας, αλλιώς συγχρονισμός μεταξύ διεργασιών (μπορεί να μην υποστηρίζεται) Χρήση: int sem_wait(sem_t *sem); μείωση τιμής κατά 1 ή αναστολή αν η τιμή είναι 0 int sem_post(sem_t *sem); αύξηση τιμής κατά 1 ή αφύπνίση νήματος αν η τιμή είναι 0 και έχει ανασταλεί νήμα int sem_trywait(sem_t *sem); μείωση τιμής κατά 1 ή επιστροφή κωδικού σφάλματος αν η τιμή είναι 0 int sem_getvalue(sem_t *sem, int *sval); Επιστρέφει την τρέχουσα τιμή του σημαφόρου – ΑΚΑΤΑΛΛΗΛΗ ΓΙΑ ΣΥΓΧΡΟΝΙΣΜΟ Καταστροφή: int sem_destroy(sem_t *sem);
Σημαφόροι για νήματα – Παράδειγμα [pthread-sem.c] typedef struct job {int weight; struct job *next;} job; #define WEIGHT_MAX 5 #define BUFFER_MAX 5 sem_t empty, full; struct job *jobQueue = NULL; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void *producer(void *param) { struct job *newJob; long myId = (long)(pthread_self()); while (1) { if ((newJob = malloc(sizeof(job))) == NULL) { fprintf(stderr, "Thread %ld: cannot allocate mem\n", myId); return NULL; } newJob->weight =rand() % WEIGHT_MAX; sleep(1 + newJob->weight + rand() % 2); printf("Thread %ld: created job, W = %d\n", myId, newJob->weight); sem_wait(&empty); pthread_mutex_lock(&mutex); enqueue(&jobQueue, newJob); pthread_mutex_unlock(&mutex); printf("Thread %ld: deposited job, W = %d\n", myId, newJob->weight); sem_post(&full);
Σημαφόροι για νήματα – Παράδειγμα [pthread-sem.c] void *consumer(void *param) { struct job *nextJob; long myId = (long)(pthread_self()); while (1) { sem_wait(&full); pthread_mutex_lock(&mutex); nextJob = dequeue(&jobQueue); pthread_mutex_unlock(&mutex); sem_post(&empty); if (nextJob == NULL) { fprintf(stderr, "Thread %ld: got NULL job!\n"); continue; } printf("Thread %ld: got job, W = %d\n", myId, nextJob->weight); sleep(1 + nextJob->weight + rand() % 2); printf("Thread %ld:consumed job, W=%d (yummy!)\n", myId, nextJob->weight); } return NULL; int main(void) { int nprod, ncons, i; pthread_t tid; sem_init(&empty, 0, BUFFER_MAX); sem_init(&full, 0, 0); printf("Enter number of producers: "); scanf("%d", &nprod); printf("Enter number of consumers: "); scanf("%d", &ncons); for (i = 0; i < ncons; i++) pthread_create(&tid, NULL, consumer, NULL); for (i = 0; i < nprod; i++) pthread_create(&tid, NULL, producer, NULL); pthread_join(tid, NULL); return 0;
Μεταβλητές συνθήκης Μοντέλο χρήσης: Εναλλακτικός τρόπος για συγχρονισμό Χρησιμοποιούνται μαζί με mutexes για να εξασφαλίσουν συγχρονισμό βάσει των τιμών των δεδομένων στα οποία έχει αποκτηθεί αποκλειστική πρόσβαση Μοντέλο χρήσης: Χρησιμοποιούμε mutex για να αποκτήσουμε αποκλειστική πρόσβαση σε ένα δεδομένο (πόρο) Αν η τιμή του δεδομένου (κατάσταση του πόρου) δεν είναι η αναμενόμενη, το νήμα αναστέλλεται στη μεταβλητή συνθήκης μέσω της λειτουργίας pthread_cond_wait, επιτρέποντας παράλληλα σε κάποιο άλλο νήμα να αποκτήσει αποκλειστική πρόσβαση στον πόρο Όταν κάποιο άλλο νήμα δώσει στα δεδομένα τιμή (φέρει τον πόρο σε κατάσταση) που θα επέτρεπε στο ανεσταλμένο νήμα να συνεχίσει, εκτελεί μία λειτουργία pthread_cond_signal για να σηματοδοτήσει το γεγονός, αφυπνίζοντας το ανεσταλμένο νήμα
Μεταβλητές συνθήκης Δήλωση μεταβλητής συνθήκης: Αρχικοποίηση: Χρήση: pthread_cond_init mycond; Αρχικοποίηση: int sem_init(pthread_cond_t *cond, const pthread_condattr_t *attr); ή pthread_cond_init mycond = PTHREAD_COND_INITIALIZER; Χρήση: int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); Αναστέλλει το νήμα, αίροντας την αποκλειστική πρόσβαση του mutex int pthread_cond_signal(pthread_cond_t *cond); αφύπνιση νήματος που έχει ανασταλεί στη μεταβλητή συνθήκης int pthread_cond_broadcast(pthread_cond_t *cond); Αφύπνιση όλων των νημάτων που έχουν ανασταλεί στη μεταβλητή συνθήκης int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); Αναστέλλει το νήμα , αίροντας την αποκλειστική πρόσβαση του mutex. Αν δεν γίνει signal μέχρι τη στιγμή που καθορίζει η παράμετρος abstime, το νήμα αφυπνίζεται αυτόματα και επιστρέφεται η τιμή ETIMEDOUT Καταστροφή: int pthread_cond_destroy(pthread_cond_t *cond);
Μεταβλητές συνθήκης – signal/wait [pthread-cond.c] #define WEIGHT_MAX 5 #define BUFFER_MAX 5 struct job *jobQueue = NULL; int itemsInQueue = 0; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t queueFull = PTHREAD_COND_INITIALIZER, queueEmpty = PTHREAD_COND_INITIALIZER; void *producer(void *param) { struct job *newJob; long myId = (long)(pthread_self()); int qlen; while (1) { if ((newJob = malloc(sizeof(job))) == NULL) { fprintf(stderr, "Thread %ld: cannot allocate mem\n", myId); return NULL; } newJob->weight = rand() % WEIGHT_MAX; sleep(1 + newJob->weight + rand() % 2); printf("Thread %ld: created job, W = %d\n", myId, newJob->weight); pthread_mutex_lock(&mutex); if (itemsInQueue == BUFFER_MAX) pthread_cond_wait(&queueFull, &mutex); enqueue(&jobQueue, newJob); if ((qlen = ++itemsInQueue) == 1) pthread_cond_signal(&queueEmpty); pthread_mutex_unlock(&mutex); printf("Thread %ld: deposited job, W = %d, qlen = %d\n", myId, newJob->weight, qlen); } return NULL;
Μεταβλητές συνθήκης – signal/wait [pthread-cond.c] void *consumer(void *param) { struct job *nextJob; long myId = (long)(pthread_self()); int qlen; while (1) { pthread_mutex_lock(&mutex); if (jobQueue == NULL) pthread_cond_wait(&queueEmpty, &mutex); nextJob = dequeue(&jobQueue); if ((qlen = --itemsInQueue) == BUFFER_MAX - 1) pthread_cond_signal(&queueFull); pthread_mutex_unlock(&mutex); if (nextJob == NULL) { fprintf(stderr, "Thread %ld: got NULL job!\n", myId); continue; } printf("Thread %ld: got job, W = %d, qlen = %d\n", myId, nextJob->weight, qlen); sleep(1 + nextJob->weight + rand() % 2); printf("Thread %ld: consumed job, W = %d (yummy!)\n", myId, nextJob->weight); } return NULL;
Μεταβλητές συνθήκης – broadcast [pthread-cond-broadcast.c] #define WEIGHT_MAX 5 #define BUFFER_MAX 5 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t readerCond = PTHREAD_COND_INITIALIZER, writerCond = PTHREAD_COND_INITIALIZER; int nreaders = 0, nwriters = 0, pendingWriters = 0; void *reader(void *param) { long myId = (long)(pthread_self()); while (1) { sleep(1 + rand() % 4); printf("Thread %ld (reader): wanna read...\n", myId); pthread_mutex_lock(&mutex); if (nwriters != 0) pthread_cond_wait(&readerCond, &mutex); nreaders++; pthread_mutex_unlock(&mutex); printf("Thread %ld (reader): starting read\n", myId); sleep(1 + rand() % 4); printf("Thread %ld (reader): done reading\n", myId); nreaders--; if (nreaders == 0) pthread_cond_signal(&writerCond); } return NULL; }
Μεταβλητές συνθήκης – broadcast [pthread-cond-broadcast.c] void *writer(void *param) { long myId = (long)(pthread_self()); while (1) { sleep(1 + rand() % 6); printf("Thread %ld (writer): wanna write...\n", myId); pthread_mutex_lock(&mutex); pendingWriters++; if (nreaders != 0 || nwriters != 0) pthread_cond_wait(&writerCond, &mutex); nwriters++; pendingWriters--; pthread_mutex_unlock(&mutex); printf("Thread %ld (writer): starting write\n", myId); sleep(1 + rand() % 4); printf("Thread %ld (writer): done writing\n", myId); nwriters--; if (pendingWriters != 0) pthread_cond_signal(&writerCond); else pthread_cond_broadcast(&readerCond); } return NULL; } int main(void) { int nread, nwrite, i; pthread_t tid; printf("Enter number of readers: "); scanf("%d", &nread); printf("Enter number of writers: "); scanf("%d", &nwrite); for (i = 0; i < nwrite; i++) pthread_create(&tid, NULL, writer, NULL); for (i = 0; i < nread; i++) pthread_create(&tid, NULL, reader, NULL); pthread_join(tid, NULL); return 0;}