Προγραμματισμός

Προγραμματισμός νημάτων Java στον πραγματικό κόσμο, Μέρος 1

Όλα τα προγράμματα Java εκτός από απλές εφαρμογές που βασίζονται σε κονσόλα είναι πολυήματα, είτε σας αρέσουν είτε όχι. Το πρόβλημα είναι ότι το Abstract Windowing Toolkit (AWT) επεξεργάζεται συμβάντα λειτουργικού συστήματος (OS) στο δικό του νήμα, έτσι οι μέθοδοι ακροατή σας εκτελούνται πραγματικά στο νήμα AWT. Αυτές οι ίδιες μέθοδοι ακροατή τυπικά έχουν πρόσβαση σε αντικείμενα στα οποία υπάρχει πρόσβαση και από το κύριο νήμα. Μπορεί να είναι δελεαστικό, σε αυτό το σημείο, να θάψετε το κεφάλι σας στην άμμο και να προσποιηθείτε ότι δεν χρειάζεται να ανησυχείτε για θέματα με θέματα, αλλά συνήθως δεν μπορείτε να το ξεφύγετε. Και, δυστυχώς, σχεδόν κανένα από τα βιβλία της Java δεν ασχολείται με θέματα σε θέματα βάθους. (Για μια λίστα με χρήσιμα βιβλία σχετικά με το θέμα, ανατρέξτε στην ενότητα Πόροι.)

Αυτό το άρθρο είναι το πρώτο σε μια σειρά που θα παρουσιάζει πραγματικές λύσεις στα προβλήματα του προγραμματισμού της Java σε ένα περιβάλλον πολλαπλών νημάτων. Είναι προσανατολισμένο σε προγραμματιστές Java που κατανοούν τα πράγματα σε επίπεδο γλώσσας (το συγχρονισμένος λέξη-κλειδί και τις διάφορες εγκαταστάσεις του Νήμα τάξη), αλλά θέλετε να μάθετε πώς να χρησιμοποιείτε αυτές τις λειτουργίες γλώσσας αποτελεσματικά.

Εξάρτηση πλατφόρμας

Δυστυχώς, η υπόσχεση της Java για ανεξαρτησία πλατφόρμας πέφτει στο πρόσωπό της στην αρένα των νημάτων. Αν και είναι δυνατό να γράψετε ένα ανεξάρτητο από πλατφόρμα πρόγραμμα πολλαπλών νημάτων Java, πρέπει να το κάνετε με τα μάτια ανοιχτά. Αυτό δεν είναι πραγματικά λάθος της Java. είναι σχεδόν αδύνατο να γράψετε ένα πραγματικά ανεξάρτητο από πλατφόρμα σύστημα σπειρώματος. (Το πλαίσιο ACE [Adaptive Communication Environment] του Doug Schmidt είναι μια καλή, αν και περίπλοκη, προσπάθεια. Δείτε πόρους για έναν σύνδεσμο με το πρόγραμμά του.) Επομένως, προτού μπορέσω να μιλήσω για θέματα σκληρού πυρήνα προγραμματισμού Java σε επόμενες δόσεις, πρέπει να συζητήστε τις δυσκολίες που παρουσιάζονται από τις πλατφόρμες στις οποίες μπορεί να εκτελείται η εικονική μηχανή Java (JVM).

Ατομική ενέργεια

Η πρώτη ιδέα σε επίπεδο λειτουργικού συστήματος που είναι σημαντικό να κατανοήσουμε είναι ατομικότητα. Μια ατομική λειτουργία δεν μπορεί να διακοπεί από άλλο νήμα. Η Java ορίζει τουλάχιστον μερικές ατομικές λειτουργίες. Συγκεκριμένα, εκχώρηση σε μεταβλητές οποιουδήποτε τύπου εκτός από μακρύς ή διπλό είναι ατομικό. Δεν χρειάζεται να ανησυχείτε για ένα νήμα που προδικάζει μια μέθοδο στη μέση της εργασίας. Στην πράξη, αυτό σημαίνει ότι δεν χρειάζεται ποτέ να συγχρονίσετε μια μέθοδο που δεν κάνει τίποτα παρά να επιστρέψετε την τιμή (ή να αντιστοιχίσετε μια τιμή) boolean ή int μεταβλητή παρουσίας. Ομοίως, μια μέθοδος που έκανε πολλούς υπολογισμούς χρησιμοποιώντας μόνο τοπικές μεταβλητές και ορίσματα και η οποία εκχώρησε τα αποτελέσματα αυτού του υπολογισμού σε μια μεταβλητή παρουσίας ως το τελευταίο πράγμα που έκανε, δεν θα έπρεπε να συγχρονιστεί. Για παράδειγμα:

τάξη some_class {int some_field; void f (some_class arg) // σκόπιμα δεν συγχρονίζονται {// Κάνετε πολλά πράγματα εδώ που χρησιμοποιούν τοπικές μεταβλητές // και ορίσματα μεθόδου, αλλά δεν έχουν πρόσβαση σε // κανένα πεδίο της κλάσης (ή καλέστε οποιεσδήποτε μεθόδους // που έχουν πρόσβαση σε οποιαδήποτε πεδία της τάξης). // ... some_field = new_value; // κάντε το τελευταίο. }} 

Από την άλλη πλευρά, κατά την εκτέλεση x = ++ ε ή x + = ε, θα μπορούσατε να προτιμηθείτε μετά την αύξηση, αλλά πριν από την ανάθεση. Για να αποκτήσετε ατομικότητα σε αυτήν την περίπτωση, θα πρέπει να χρησιμοποιήσετε τη λέξη-κλειδί συγχρονισμένος.

Όλα αυτά είναι σημαντικά επειδή το γενικό κόστος του συγχρονισμού μπορεί να είναι ασήμαντο και μπορεί να διαφέρει από OS σε OS. Το παρακάτω πρόγραμμα δείχνει το πρόβλημα. Κάθε βρόχος καλεί επαναλαμβανόμενα μια μέθοδο που εκτελεί τις ίδιες λειτουργίες, αλλά μία από τις μεθόδους (κλείδωμα ()) είναι συγχρονισμένο και το άλλο (όχι_κλείδωμα ()) δεν είναι. Χρησιμοποιώντας το JDK "performance-pack" VM που εκτελείται στα Windows NT 4, το πρόγραμμα αναφέρει διαφορά 1,2 δευτερολέπτων στο χρόνο εκτέλεσης μεταξύ των δύο βρόχων ή περίπου 1,2 μικροδευτερόλεπτα ανά κλήση. Αυτή η διαφορά μπορεί να μην μοιάζει πολύ, αλλά αντιπροσωπεύει αύξηση 7,25 τοις εκατό στο χρόνο κλήσης. Φυσικά, η ποσοστιαία αύξηση μειώνεται καθώς η μέθοδος λειτουργεί περισσότερο, αλλά ένας σημαντικός αριθμός μεθόδων - τουλάχιστον στα προγράμματά μου - είναι μόνο μερικές γραμμές κώδικα.

εισαγωγή java.util. *; συγχρονισμός τάξης {  συγχρονισμένο int κλείδωμα (int a, int b) {return a + b;} int not_locking (int a, int b) {return a + b;}  ιδιωτικό στατικό τελικό int ITERATIONS = 1000000; static public void main (String [] args) {synch tester = new synch (); διπλή έναρξη = νέα ημερομηνία (). getTime ();  για (long i = ITERATIONS; --i> = 0;) tester.locking (0,0);  double end = νέο Ημερομηνία (). getTime (); double locking_time = end - έναρξη; έναρξη = νέο Ημερομηνία (). getTime ();  για (long i = ITERATIONS; --i> = 0;) tester.not_locking (0,0);  end = νέο Ημερομηνία (). getTime (); double not_locking_time = τέλος - έναρξη; double time_in_synchronization = locking_time - not_locking_time; System.out.println ("Χάθηκε χρόνος για συγχρονισμό (χιλιοστά.):" + Time_in_synchronization); System.out.println ("Κλείδωμα γενικά ανά κλήση:" + (time_in_synchronization / ITERATIONS)); System.out.println (not_locking_time / locking_time * 100.0 + "% αύξηση"); }} 

Αν και το HotSpot VM υποτίθεται ότι αντιμετωπίζει το πρόβλημα συγχρονισμού-overhead, το HotSpot δεν είναι δωρεάν - πρέπει να το αγοράσετε. Εάν δεν διαθέτετε άδεια και αποστέλλετε το HotSpot με την εφαρμογή σας, δεν υπάρχει καμία ιδέα για το τι θα είναι το VM στην πλατφόρμα προορισμού και φυσικά θέλετε να εξαρτάται όσο το δυνατόν λιγότερο από την ταχύτητα εκτέλεσης του προγράμματος σας από το VM που το εκτελεί. Ακόμα κι αν δεν υπήρχαν προβλήματα αδιεξόδου (τα οποία θα συζητήσω στην επόμενη δόση αυτής της σειράς), η ιδέα ότι πρέπει να "συγχρονίσετε τα πάντα" είναι απλώς λανθασμένη.

Ταυτότητα εναντίον παραλληλισμού

Το επόμενο ζήτημα που σχετίζεται με το λειτουργικό σύστημα (και το κύριο πρόβλημα όταν πρόκειται για γραφή Java ανεξάρτητη από πλατφόρμα) σχετίζεται με τις έννοιες του συγχρονισμός και παραλληλισμός. Τα ταυτόχρονα συστήματα πολλαπλών νημάτων δίνουν την εμφάνιση πολλών εργασιών που εκτελούνται ταυτόχρονα, αλλά αυτές οι εργασίες χωρίζονται πραγματικά σε κομμάτια που μοιράζονται τον επεξεργαστή με κομμάτια από άλλες εργασίες. Το παρακάτω σχήμα απεικονίζει τα ζητήματα. Σε παράλληλα συστήματα, δύο εργασίες εκτελούνται στην πραγματικότητα ταυτόχρονα. Ο παραλληλισμός απαιτεί ένα σύστημα πολλαπλών CPU.

Εκτός αν αφιερώνετε πολύ χρόνο αποκλεισμένο, περιμένοντας την ολοκλήρωση των λειτουργιών εισόδου / εξόδου, ένα πρόγραμμα που χρησιμοποιεί πολλαπλά ταυτόχρονα νήματα θα εκτελείται συχνά πιο αργά από ένα αντίστοιχο πρόγραμμα με ένα νήμα, αν και συχνά θα είναι καλύτερα οργανωμένο από το αντίστοιχο - έκδοση νήματος. Ένα πρόγραμμα που χρησιμοποιεί πολλαπλά νήματα που εκτελούνται παράλληλα σε πολλούς επεξεργαστές θα εκτελείται πολύ πιο γρήγορα.

Αν και η Java επιτρέπει το νήμα να εφαρμοστεί εξ ολοκλήρου στο VM, τουλάχιστον θεωρητικά, αυτή η προσέγγιση θα αποκλείει τυχόν παραλληλισμό στην εφαρμογή σας. Εάν δεν χρησιμοποιήθηκαν νήματα σε επίπεδο λειτουργικού συστήματος, το λειτουργικό σύστημα θα εξέταζε την παρουσία VM ως εφαρμογή ενός νήματος, η οποία πιθανότατα θα ήταν προγραμματισμένη σε έναν μόνο επεξεργαστή. Το καθαρό αποτέλεσμα θα ήταν ότι δεν θα εκτελούνται παράλληλα δύο νήματα Java με την ίδια παρουσία VM, ακόμα κι αν είχατε πολλαπλούς επεξεργαστές και η VM σας ήταν η μόνη ενεργή διαδικασία. Φυσικά, δύο παρουσίες του VM που εκτελούν ξεχωριστές εφαρμογές θα μπορούσαν να εκτελούνται παράλληλα, αλλά θέλω να κάνω καλύτερα από αυτό. Για να πάρουμε τον παραλληλισμό, το VM πρέπει χαρτογράφηση νήματα Java σε νήματα OS. Έτσι, δεν μπορείτε να αγνοήσετε τις διαφορές μεταξύ των διαφόρων μοντέλων σπειρώματος εάν η ανεξαρτησία της πλατφόρμας είναι σημαντική.

Ευθυγραμμίστε τις προτεραιότητές σας

Θα δείξω τους τρόπους με τους οποίους τα θέματα που μόλις ανέφερα μπορούν να επηρεάσουν τα προγράμματά σας συγκρίνοντας δύο λειτουργικά συστήματα: Solaris και Windows NT.

Η Java, θεωρητικά τουλάχιστον, παρέχει δέκα επίπεδα προτεραιότητας για νήματα. (Εάν δύο ή περισσότερα νήματα περιμένουν να τρέξουν, θα εκτελεστεί ένα με το υψηλότερο επίπεδο προτεραιότητας.) Στο Solaris, το οποίο υποστηρίζει 231 επίπεδα προτεραιότητας, αυτό δεν αποτελεί πρόβλημα (αν και οι προτεραιότητες του Solaris μπορεί να είναι δύσκολο να χρησιμοποιηθούν - περισσότερα σε αυτό σε λίγο). Το NT, από την άλλη πλευρά, διαθέτει επτά επίπεδα προτεραιότητας και αυτά πρέπει να χαρτογραφηθούν στα δέκα της Java. Αυτή η χαρτογράφηση είναι απροσδιόριστη, έτσι υπάρχουν πολλές δυνατότητες. (Για παράδειγμα, τα επίπεδα προτεραιότητας Java 1 και 2 ενδέχεται να αντιστοιχούν και τα δύο στο επίπεδο προτεραιότητας NT 1 και τα επίπεδα προτεραιότητας Java 8, 9 και 10 ενδέχεται να αντιστοιχούν όλα στο επίπεδο NT 7.)

Η έλλειψη επιπέδων προτεραιότητας της NT είναι πρόβλημα εάν θέλετε να χρησιμοποιήσετε την προτεραιότητα για τον έλεγχο του προγραμματισμού. Τα πράγματα γίνονται ακόμη πιο περίπλοκα από το γεγονός ότι τα επίπεδα προτεραιότητας δεν είναι σταθερά. Το NT παρέχει έναν μηχανισμό που ονομάζεται ενίσχυση προτεραιότητας, την οποία μπορείτε να απενεργοποιήσετε με μια κλήση συστήματος C, αλλά όχι από την Java. Όταν είναι ενεργοποιημένη η ενίσχυση προτεραιότητας, το NT αυξάνει την προτεραιότητα ενός νήματος από ένα απροσδιόριστο ποσό για ένα απροσδιόριστο χρονικό διάστημα κάθε φορά που εκτελεί ορισμένες κλήσεις συστήματος που σχετίζονται με I / O. Στην πράξη, αυτό σημαίνει ότι το επίπεδο προτεραιότητας ενός νήματος θα μπορούσε να είναι υψηλότερο από ό, τι νομίζετε, επειδή αυτό το νήμα έτυχε να εκτελέσει μια λειτουργία I / O σε μια δύσκολη στιγμή.

Το σημείο της ενίσχυσης προτεραιότητας είναι να αποτρέψει τα νήματα που κάνουν επεξεργασία υποβάθρου να επηρεάσουν την φαινομενική ανταπόκριση των εργασιών βαρύ UI. Άλλα λειτουργικά συστήματα έχουν πιο εξελιγμένους αλγόριθμους που συνήθως μειώνουν την προτεραιότητα των διαδικασιών φόντου. Το μειονέκτημα αυτού του σχήματος, ιδιαίτερα όταν εφαρμόζεται σε επίπεδο ανά νήμα και όχι σε επίπεδο διαδικασίας, είναι ότι είναι πολύ δύσκολο να χρησιμοποιηθεί προτεραιότητα για να καθοριστεί πότε θα εκτελεστεί ένα συγκεκριμένο νήμα.

Χειροτερεύει.

Στο Solaris, όπως συμβαίνει σε όλα τα συστήματα Unix, οι διαδικασίες έχουν προτεραιότητα, καθώς και νήματα. Τα νήματα των διαδικασιών υψηλής προτεραιότητας δεν μπορούν να διακοπεί από τα νήματα των διαδικασιών χαμηλής προτεραιότητας. Επιπλέον, το επίπεδο προτεραιότητας μιας δεδομένης διαδικασίας μπορεί να περιοριστεί από έναν διαχειριστή συστήματος, έτσι ώστε μια διαδικασία χρήστη να μην διακόψει κρίσιμες διαδικασίες λειτουργικού συστήματος. Το NT δεν υποστηρίζει κανένα από αυτά. Μια διαδικασία NT είναι απλώς ένας χώρος διευθύνσεων. Δεν έχει καθόλου προτεραιότητα και δεν είναι προγραμματισμένη. Το σύστημα προγραμματίζει νήματα. τότε, εάν ένα δεδομένο νήμα εκτελείται με μια διαδικασία που δεν είναι στη μνήμη, η διαδικασία αντικαθίσταται. Οι προτεραιότητες νήματος ΝΤ εμπίπτουν σε διάφορες "τάξεις προτεραιότητας", οι οποίες κατανέμονται σε ένα σύνολο πραγματικών προτεραιοτήτων. Το σύστημα μοιάζει με αυτό:

Οι στήλες είναι πραγματικά επίπεδα προτεραιότητας, μόνο 22 από τα οποία πρέπει να κοινοποιούνται από όλες τις εφαρμογές. (Οι άλλες χρησιμοποιούνται από την ίδια τη ΝΤ.) Οι σειρές είναι κλάσεις προτεραιότητας. Τα νήματα που εκτελούνται σε μια διαδικασία συνδεδεμένη στην αδρανή κατηγορία προτεραιότητας εκτελούνται στα επίπεδα 1 έως 6 και 15, ανάλογα με το καθορισμένο επίπεδο λογικής προτεραιότητας. Τα νήματα μιας διαδικασίας που ομαδοποιούνται ως κανονική κατηγορία προτεραιότητας θα εκτελούνται στα επίπεδα 1, 6 έως 10 ή 15 εάν η διαδικασία δεν έχει την εστίαση εισόδου. Εάν έχει την εστίαση εισόδου, τα νήματα εκτελούνται στα επίπεδα 1, 7 έως 11 ή 15. Αυτό σημαίνει ότι ένα νήμα υψηλής προτεραιότητας μιας διαδικασίας κλάσης αδρανούς προτεραιότητας μπορεί να προεπιλέξει ένα νήμα χαμηλής προτεραιότητας μιας διαδικασίας κανονικής κατηγορίας προτεραιότητας, αλλά μόνο αν αυτή η διαδικασία εκτελείται στο παρασκήνιο. Παρατηρήστε ότι μια διαδικασία που εκτελείται στην κατηγορία "υψηλής" προτεραιότητας έχει μόνο έξι επίπεδα προτεραιότητας στη διάθεσή της. Οι άλλες τάξεις έχουν επτά.

Το NT δεν παρέχει τρόπο περιορισμού της κατηγορίας προτεραιότητας μιας διαδικασίας. Οποιοδήποτε νήμα σε οποιαδήποτε διαδικασία στο μηχάνημα μπορεί να αναλάβει τον έλεγχο του κουτιού ανά πάσα στιγμή, ενισχύοντας τη δική του κατηγορία προτεραιότητας. δεν υπάρχει άμυνα ενάντια σε αυτό.

Ο τεχνικός όρος που χρησιμοποιώ για να περιγράψω την προτεραιότητα του ΝΤ είναι ανίενο χάος. Στην πράξη, η προτεραιότητα είναι ουσιαστικά άχρηστη στο NT.

Τι πρέπει λοιπόν να κάνει ένας προγραμματιστής; Μεταξύ του περιορισμένου αριθμού επιπέδων προτεραιότητας της NT και της ανεξέλεγκτης ενίσχυσης προτεραιότητας, δεν υπάρχει απολύτως ασφαλής τρόπος για ένα πρόγραμμα Java να χρησιμοποιεί επίπεδα προτεραιότητας για προγραμματισμό. Ένας εφαρμόσιμος συμβιβασμός είναι να περιοριστείτε Νήμα.MAX_PRIORITY, Νήμα.MIN_PRIORITY, και Νήμα.NORM_PRIORITY όταν καλείτε setPriority (). Αυτός ο περιορισμός αποφεύγει τουλάχιστον το πρόβλημα 10-επιπέδων-χαρτογράφησης-σε-7-επιπέδων. Υποθέτω ότι θα μπορούσατε να χρησιμοποιήσετε το Όνομα ιδιότητα συστήματος για τον εντοπισμό NT και, στη συνέχεια, καλέστε μια εγγενή μέθοδο για να απενεργοποιήσετε την ενίσχυση προτεραιότητας, αλλά αυτό δεν θα λειτουργήσει εάν η εφαρμογή σας εκτελείται στον Internet Explorer, εκτός εάν χρησιμοποιείτε επίσης την προσθήκη Sun VM (Το VM της Microsoft χρησιμοποιεί μια μη τυπική εφαρμογή εγγενών μεθόδων.) Σε κάθε περίπτωση, μισώ να χρησιμοποιώ εγγενείς μεθόδους. Συνήθως αποφεύγω το πρόβλημα όσο το δυνατόν περισσότερο βάζοντας τα περισσότερα νήματα NORM_PRIORITY και χρησιμοποιώντας μηχανισμούς προγραμματισμού διαφορετικούς από την προτεραιότητα. (Θα συζητήσω μερικά από αυτά σε μελλοντικές δόσεις αυτής της σειράς.)

Συνεργάζονται!

Υπάρχουν συνήθως δύο μοντέλα σπειρώματος που υποστηρίζονται από λειτουργικά συστήματα: συνεργατικά και προληπτικά.

Το συνεταιριστικό μοντέλο πολλαπλών νημάτων

Σε ένα συνεργατική σύστημα, ένα νήμα διατηρεί τον έλεγχο του επεξεργαστή του έως ότου αποφασίσει να το εγκαταλείψει (που μπορεί να μην είναι ποτέ). Τα διάφορα νήματα πρέπει να συνεργαστούν μεταξύ τους ή όλα, αλλά ένα από τα νήματα θα είναι "λιμοκτονούν" (που σημαίνει, δεν θα δοθεί ποτέ η ευκαιρία να τρέξει). Ο προγραμματισμός στα περισσότερα συστήματα συνεργασίας γίνεται αυστηρά από το επίπεδο προτεραιότητας. Όταν το τρέχον νήμα εγκαταλείψει τον έλεγχο, το νήμα αναμονής υψηλότερης προτεραιότητας παίρνει τον έλεγχο. (Εξαίρεση σε αυτόν τον κανόνα είναι τα Windows 3.x, τα οποία χρησιμοποιούν ένα συνεργατικό μοντέλο, αλλά δεν έχουν μεγάλο χρονοδιάγραμμα. Το παράθυρο που έχει την εστίαση παίρνει τον έλεγχο.)