Οι κανόνες προγραμματισμού εργασίας μπορούν να χρησιμοποιηθούν για τον έλεγχο της ώρας εκτέλεσης των εργασιών σας σε σχέση με άλλες εργασίες. Ειδικότερα, οι κανόνες προγραμματισμού σάς επιτρέπουν να αποφεύγετε την ταυτοχρονισμένη εκτέλεση πολλαπλών εργασιών σε περιπτώσεις κατά τις οποίες ο ταυτοχρονισμός μπορεί να οδηγήσει σε ασυνεπή αποτελέσματα. Σας επιτρέπουν επίσης να εγγυάστε για τη σειρά εκτέλεσης μιας σειράς εργασιών. Η δυνατότητες των κανόνων προγραμματισμού απεικονίζονται καλύτερα στο παράδειγμα. Ας ξεκινήσουμε με τον καθορισμό δύο εργασιών οι οποίες χρησιμοποιούνται για την ταυτοχρονισμένη ενεργοποίηση και απενεργοποίηση του διακόπτη μιας λάμπας:
public class LightSwitch { private boolean isOn = false; public boolean isOn() { return isOn; } public void on() { new LightOn().schedule(); } public void off() { new LightOff().schedule(); } class LightOn extends Job { public LightOn() { super("Turning on the light"); } public IStatus run(IProgressMonitor monitor) { System.out.println("Turning the light on"); isOn = true; return Status.OK_STATUS; } } class LightOff extends Job { public LightOff() { super("Turning off the light"); } public IStatus run(IProgressMonitor monitor) { System.out.println("Turning the light off"); isOn = false; return Status.OK_STATUS; } } }
Παρουσιάζουμε τώρα ένα απλό πρόγραμμα με το οποίο δημιουργείται ένας διακόπτης λάμπας που ενεργοποιείται και απενεργοποιείται ξανά:
LightSwitch light = new LightSwitch(); light.on(); light.off(); System.out.println("The light is on? " + switch.isOn());
Αν εκτελέσουμε αυτό το σύντομο πρόγραμμα αρκετές φορές, θα λάβουμε τελικά το ακόλουθο αποτέλεσμα:
Turning the light off Turning the light on The light is on? true
Πως είναι δυνατόν όμως; Είπαμε στη λάμπα να ανάψει και να σβήσει, και συνεπώς η τελική κατάσταση λειτουργίας της θα πρέπει να είναι απενεργοποιημένη! Το πρόβλημα έγκειται στο γεγονός ότι δεν υπήρχε τίποτα που να εμποδίζει την ταυτόχρονη εκτέλεση της εργασίας LightOff με την εργασία LightOn. Έτσι, ακόμα κι αν η εργασία "ενεργοποίηση" ήταν αυτή που προγραμματίστηκε πρώτα, η ταυτόχρονη εκτέλεσή τους σημαίνει ότι δεν υπάρχει περίπτωση να προβλεφθεί η ακριβής σειρά εκτέλεσης των δύο αυτών ταυτόχρονων εργασιών. Αν η εργασία LightOff διακόψει την εκτέλεσή της πριν από την εργασία LightOn, λαμβάνουμε αυτό το μη-έγκυρο αποτέλεσμα. Αυτό που χρειαζόμαστε είναι ένας τρόπος για να αποφύγουμε την ταυτόχρονη εκτέλεση των δύο αυτών εργασιών, και εδώ ακριβώς είναι που εμφανίζονται οι κανόνες προγραμματισμού.
Μπορούμε να διορθώσουμε αυτό το παράδειγμα δημιουργώντας έναν απλό κανόνα προγραμματισμού που ενεργεί ως ένας σημαφόρος αμοιβαίου αποκλεισμού (γνωστός επίσης και ως δυαδικός σημαφόρος):
class Mutex implements ISchedulingRule { public boolean isConflicting(ISchedulingRule rule) { return rule == this; } public boolean contains(ISchedulingRule rule) { return rule == this; } }
Αυτός ο κανόνας προστίθεται στη συνέχεια στις δύο εργασίες για το διακόπτη της λάμπας από το προηγούμενο παράδειγμά μας
public class LightSwitch { final MutextRule rule = new MutexRule(); ... class LightOn extends Job { public LightOn() { super("Turning on the light"); setRule(rule); } ... } class LightOff extends Job { public LightOff() { super("Turning off the light"); setRule(rule); } ... } }
Κατά τον προγραμματισμό των δύο εργασιών για το διακόπτη της λάμπας, η υποδομή της εργασίας καλεί τη μέθοδο isConflicting για να συγκρίνει τους κανόνες προγραμματισμού των δύο εργασιών. Θα εντοπίσει ότι οι δύο εργασίες έχουν κανόνες προγραμματισμού που βρίσκονται σε διένεξη, και θα βεβαιωθεί ότι εκτελούνται με τη σωστή σειρά. Επίσης θα βεβαιώσει ότι δεν εκτελούνται ποτέ ταυτόχρονα. Αν εκτελέσετε το πρόγραμμα του παραδείγματος ένα εκατομμύριο φορές, θα λάβετε το ίδιο αποτέλεσμα:
Turning the light on Turning the light off The light is on? false
Μπορείτε επίσης να χρησιμοποιείτε ανεξάρτητα τους κανόνες από διάφορες εργασίες ως έναν γενικό μηχανισμό κλειδώματος. Το ακόλουθο παράδειγμα λαμβάνει έναν κανόνα μέσα σε μια ενότητα try/finally, αποτρέποντας σε άλλα νήματα και εργασίες να εκτελούνται με αυτόν τον κανόνα για όσο διαρκούν οι κλήσεις μεταξύ των beginRule και endRule.
IJobManager manager = Platform.getJobManager(); try { manager.beginRule(rule, monitor); ... do some work ... } finally { manager.endRule(rule); }
Θα πρέπει να είστε ιδιαίτερα προσεκτικοί κατά τη δέσμευση και αποδέσμευση κανόνων προγραμματισμού όταν χρησιμοποιείτε ένα τέτοιο μοτίβο κωδικοποίησης. Αν αποτύχετε να ολοκληρώσετε έναν κανόνα για τον οποίο έχετε καλέσει το beginRule, θα κλειδώσετε αυτόν τον κανόνα για πάντα.
Αν και το API μιας εργασίας καθορίζει τη συμφωνία των κανόνων προγραμματισμού, δεν παρέχει πραγματικά καμία υλοποίηση κανόνα προγραμματισμού. Βασικά, η γενική υποδομή δεν μπορεί να γνωρίζει ποια σύνολα εργασιών είναι κατάλληλα για ταυτόχρονη εκτέλεση. Από προεπιλογή, οι εργασίες δεν έχουν κανόνες προγραμματισμού και μια προγραμματισμένη εργασία εκτελείται τόσο γρήγορα όσο γρήγορα δημιουργείται το νήμα που θα την εκτελέσει.
Όταν μια εργασία έχει έναν κανόνα προγραμματισμού, η μέθοδος isConflicting χρησιμοποιείται για να προσδιορίσει εάν ο κανόνας έρχεται σε διένεξη με τους κανόνες άλλων εργασιών που εκτελούνται εκείνη τη στιγμή. Έτσι, η υλοποίησή σας του isConflicting μπορεί να καθορίσει με ακρίβεια τον ασφαλή χρόνο εκτελέσης της εργασίας σας. Στο παράδειγμα του διακόπτη της λάμπας, η υλοποίηση isConflicting χρησιμοποιεί απλά μια σύγκριση ταυτότητας με τον παρεχόμενο κανόνα. Αν κάποια εργασία έχει πανομοιότυπο κανόνα, δεν θα εκτελούνται ταυτόχρονα. Κατά την εγγραφή των δικών σας κανόνων προγραμματισμού, βεβαιωθείτε ότι διαβάζετε προσεκτικά και ακολουθείτε τη συμφωνία API για το isConflicting.
Αν η εργασία σας έχει μη-συσχετισμένους περιορισμούς, μπορείτε να συνθέσετε μαζί πολλαπλούς κανόνες προγραμματισμού χρησιμοποιώντας την κλάση MultiRule. Για παράδειγμα, αν η εργασία σας χρειάζεται να ενεργοποιήσει το διακόπτη μιας λάμπας και να εγγράψει πληροφορίες στη δίοδο επικοινωνίας ενός δικτύου, μπορεί να έχει έναν κανόνα για αυτόν το διακόπτη της λάμπας και έναν κανόνα για την πρόσβαση εγγραφής στη δίοδο επικοινωνίας, συνδυασμένους σε ένα μεμονωμένο κανόνα, με χρήση της μεθόδου κατασκευής MultiRule.combine.
Συζητήσαμε τη μέθοδο isConflicting της κλάσης ISchedulingRule, αλλά παραλείψαμε να αναφερθούμε στη μέθοδο contains. Αυτή η μέθοδος χρησιμοποιείται για μια αρκετά εξειδικευμένη εφαρμογή κανόνων προγραμματισμού η οποία δεν είναι ιδιαίτερα δημοφιλής σε πολλούς πελάτες. Οι κανόνες προγραμματισμού μπορούν λογικά να συντεθούν μέσα σε ιεραρχίες για τον έλεγχο της πρόσβασης σε φυσικά ιεραρχικούς πόρους. Το πιο απλό παράδειγμα απεικόνισης αυτής της έννοιας είναι ένα σύστημα αρχείων που βασίζεται σε διακλάδωση. Αν κάποια εφαρμογή επιθυμεί να δεσμεύσει ένα αποκλειστικό κλείδωμα σε έναν κατάλογο, συνήθως σημαίνει ότι επιθυμεί επίσης την αποκλειστική πρόσβαση στα αρχεία και τους υπο-καταλόγους που βρίσκονται μέσα σε αυτόν τον κατάλογο. Η μέθοδος contains χρησιμοποιείται για να καθορίσει την ιεραρχική σχέση μεταξύ κλειδωμάτων. Αν δεν χρειάζεται να δημιουργήσετε ιεραρχίες κλειδωμάτων, μπορείτε να υλοποιήσετε τη μέθοδο contains για να καλέσετε απλά την κλάση isConflicting.
Ακολουθεί ένα παράδειγμα ιεραρχικού κλειδώματος για τον έλεγχο της πρόσβασης εγγραφής στις ρουτίνες χειρισμού java.io.File.
public class FileLock implements ISchedulingRule { private String path; public FileLock(java.io.File file) { this.path = file.getAbsolutePath(); } public boolean contains(ISchedulingRule rule) { if (this == rule) return true; if (rule instanceof FileLock) return path.startsWith(((FileLock) rule).path); if (rule instanceof MultiRule) { MultiRule multi = (MultiRule) rule; ISchedulingRule[] children = multi.getChildren(); for (int i = 0; i < children.length; i++) if (!contains(children[i])) return false; return true; } return false; } public boolean isConflicting(ISchedulingRule rule) { if (!(rule instanceof FileLock)) return false; String otherPath = ((FileLock)rule).path; return path.startsWith(otherPath) || otherPath.startsWith(path); } }
Η μέθοδος contains εμφανίζεται εάν κάποιο νήμα επιχειρήσει να δεσμεύσει έναν δεύτερο κανόνα όταν ήδη έχει στην κατοχή του έναν άλλο κανόνα. Για να αποφύγετε τη πιθανότητα κλειδώματος, οποιοδήποτε δεδομένο νήμα μπορεί να κατέχει, μια οποιαδήποτε δεδομένη στιγμή, έναν μόνο κανόνα προγραμματισμού. Αν ένα νήμα καλεί beginRule ενώ κατέχει έναν κανόνα, είτε μέσω μιας προηγούμενης κλήσης στην κλάση beginRule ή μέσω της εκτέλεσης μιας εργασίας με έναν κανόνα προγραμματισμού, θα πρέπει να συμβουλεύεστε τη μέθοδο contains προκειμένου να δείτε εάν οι δύο κανόνες είναι ισοδύναμοι. Αν η μέθοδος contains για τον κανόνα που ήδη έχει δεσμευθεί επιστρέψει την τιμή true, η κλήση beginRule είναι επιτυχής. Αν η μέθοδος contains επιστρέψει false θα εμφανιστεί σφάλμα.
Για να γίνουμε πιο συγκεκριμένοι, ας πούμε ότι κάποιο νήμα κατέχει τον κανόνα FileLock του παραδείγματός μας σε έναν κατάλογο που βρίσκεται στη θέση "c:\temp". Ενώ κατέχει τον κανόνα, αυτό το νήμα μπορεί μόνο να τροποποιήσει αρχεία μέσα στην υπο-διακλάδωση του συγκεκριμένου υποκαταλόγου. Αν επιχειρήσει να τροποποιήσει αρχεία σε άλλους καταλόγους στη θέση "c:\temp", θα αποτύχει. Έτσι, ένας κανόνας προγραμματισμού είναι μια συγκεκριμενη προδιαγραφή σχετικά με το τι μια εργασία ή ένα νήμα επιτρέπεται ή δεν επιτρέπεται να κάνει. Η παραβίαση αυτής της προδιαγραφής θα έχει ως αποτέλεσμα μια εξαίρεση στο περιβάλλον εκτέλεσης. Στο περιεχόμενο του ταυτοχρονισμού, αυτή η τεχνική είναι γνωστή ως κλείδωμα δύο σταδίων. Στο σχήμα κλειδώματος δύο σταδίων, μια διαδικασία πρέπει να καθορίζει εκ των προτέρων όλα τα κλειδώματα που θα χρειαστεί για μια συγκεκριμένη εργασία, μια στιγμή έπειτα από την οποία δεν επιτρέπεται η δέσμευση επιπλέον κλειδωμάτων κατά τη διάρκεια της λειτουργίας. Το κλείδωμα δύο σταδίων απαλείφει τη συνθήκη δέσμευσης-και-αναμονής η οποία αποτελεί προαπαιτούμενο για ένα αδιέξοδο κυκλικής αναμονής. Κατά συνέπεια, ένα σύστημα δεν μπορεί να χρησιμοποιήσει μόνο κανόνες προγραμματισμού ως στοιχειώδες κλείδωμα για την εισαγωγή ενός αδιεξόδου.