====== Ταυτόχρονος Προγραμματισμός (Νήματα) ====== Στον ταυτόχρονο προγραμματισμό, υπάρχουν δύο βασικές μονάδες εκτέλεσης: οι διεργασίες (processes) και τα νήματα (threads). Στη γλώσσα προγραμματισμού Java, ο ταυτόχρονος προγραμματισμός επιτυγχάνεται μέσω της χρήσης νημάτων. Ένα υπολογιστικό σύστημα έχει συνήθως πολλές ενεργές διεργασίες και νήματα που εκτελούνται ταυτόχρονα. Αυτό ισχύει ακόμη και σε συστήματα που έχουν μόνο έναν πυρήνα στο υλικό τους, και ως εκ τούτου μπορούν να εκτελέσουν μόνο ένα νήμα ή διεργασία σε κάθε χρονική στιγμή. Ο χρόνος επεξεργασίας μεταξύ των διεργασιών μοιράζεται μέσω του χρονοπρογραμματιστή (//scheduler//) του λειτουργικού συστήματος. Μία διεργασία έχει αυτόνομο περιβάλλον εκτέλεσης, δηλ έχει την δική της εικονική μνήμη και τις δικές της μεταβλητές. Συχνά οι διεργασίες ταυτίζονται με τα προγράμματα ή τις εφαρμογές, πράγμα που δεν είναι πάντα αληθές καθώς ένα πρόγραμμα μπορεί να είναι αποτέλεσμα περισσότερων διεργασιών οι οποίες επικοινωνούν μεταξύ τους μέσω κατάλληλων διεπαφών επικοινωνίας μεταξύ διεργασιών (Interprocess Communication - IPC), όπως ορίζει το κάθε λειτουργικό σύστημα. ===== Νήματα (Threads) ===== Τα νήματα αναφέρονται συχνά και ως lightweight processes. Σε αναλογία με τις διεργασίες τα νήματα παρέχουν ένα περιβάλλον εκτέλεσης, αλλά απαιτούν λιγότερους πόρους κατά την δημιουργία τους σε σχέση με μία διεργασία. Τα νήματα ζουν μέσα στις διεργασίες (κάθε διεργασία έχει τουλάχιστον ένα νήμα) και μοιράζονται τους πόρους της διεργασίας, δηλαδή την μνήμη και τα αρχεία που έχει ανοίξει η διεργασία. Η πολυνηματική εκτέλεση αποτελεί ένα από τα βασικά χαρακτηριστικά της πλατφόρμας Java. Κάθε Java εφαρμογή έχει ένα αρχικό νήμα εκτέλεσης, το οποίο εκκινεί μέσω της μεθόδου //main//. Το νήμα αυτό έχει τη δυνατότητα να δημιουργήσει πρόσθετα νήματα, μέσω της διαδικασίας που περιγράφεται παρακάτω. ===== Δημιουργία και εκτέλεση νημάτων ===== Υπάρχουν δύο παρεμφερείς τρόποι για την δημιουργία και εκτέλεση νημάτων. Μία κλάση μπορεί να δημιουργήσει ένα νήμα είτε υλοποιώντας το interface [[http://docs.oracle.com/javase/8/docs/api/java/lang/Runnable.html|java.lang.Runnable]] είτε επεκτείνοντας την κλάση [[http://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html|java.lang.Thread]] (κλάση που υλοποιεί το παραπάνω interface). Και οι δύο τρόποι δημιουργίας και εκτέλεσης νημάτων παρατίθενται παρακάτω: public class HelloRunnable implements Runnable { public void run() { System.out.println("Hello from a thread!"); } public static void main(String args[]) { (new Thread(new HelloRunnable())).start(); } } public class HelloThread extends Thread { public void run() { System.out.println("Hello from a thread!"); } public static void main(String args[]) { (new HelloThread()).start(); } } Παρατηρείστε ότι και τα δύο παραπάνω παραδείγματα καλούν την μέθοδο [[http://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#start()|Thread.start()]]. Από τα δύο παραπάνω τρόπους ο πρώτος έχει το πλεονέκτημα ότι η νηματική κλάση δεν κληρονομεί την κλάση [[http://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html|Thread]] (επομένως μπορεί να κληρονομήσει μία άλλη κλάση) και το μειονέκτημα ότι η νηματική κλάση δεν μπορεί να χρησιμοποιήσει καμία από τις μεθόδους που υλοποιεί η κλάση [[http://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html|Thread]] (θα τις δούμε παρακάτω). ===== Αναβάλλοντας την εκτέλεση ενός νήματος μέσω της μεθόδου sleep() ===== Η μέθοδος [[http://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#sleep-long-|sleep]] δίνει την δυνατότητα σε ένα νήμα να σταματήσει την εκτέλεση του για ένα συγκεκριμένο χρονικό διάστημα. Κατά το διάστημα αυτό, άλλα νήματα της τρέχουσας διεργασίας μπορούν να εκτελεστούν. Θα πρέπει να έχουμε υπόψη μας ότι η χρονική διάρκεια κατά την οποία θα παύσει η εκτέλεση του νήματος μπορεί να έχει μικρές αποκλίσεις από τη χρονική διάρκεια που προσδιορίζεται στα ορίσματα της μεθόδου. Δείτε το παρακάτω παράδειγμα χρήσης της μεθόδου [[http://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#sleep-long-|sleep]]. public class SleepMessages { public static void main(String args[]) throws InterruptedException { String importantInfo[] = { "Peter Pan", "leader of the Lost Boys", "goes to Neverland" }; for (int i=0; i Ένα νήμα μπορεί να καλέσει την μέθοδο [[http://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#sleep-long-|sleep]] για να κοιμηθεί το ίδιο και όχι για να κοιμίσει κάποιο άλλο νήμα. Ακόμη και εάν κληθεί η μέθοδος //sleep// για άλλο νήμα αυτό θα κοιμηθεί το νήμα που την κάλεσε. ===== Αφύπνιση ενός νήματος μέσω της μεθόδου interrupt() ===== Μία διακοπή (//interrupt//) αποτελεί ένδειξη προς την διεργασία να σταματήσει να κάνει αυτό που κάνει και να εκκινήσει κάτι διαφορετικό ή να τερματίσει. Η java υποστηρίζει την μέθοδο [[http://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#interrupt()|Thread.interrupt()]] για την αποστολή ενός σήματος διακοπής σε ένα νήμα. Εάν ένα νήμα έχει καλέσει τη μέθοδο //sleep// η κλήση της //interrupt// για το νήμα αυτό, θα το αφυπνίσει και θα παραχθεί ένα [[http://docs.oracle.com/javase/7/docs/api/java/lang/InterruptedException.html|java.lang.InterruptedException]] για το νήμα που κοιμόταν. Για τον λόγο αυτό, η κλήση της //sleep// θα πρέπει να γίνεται πάντα μέσα σε ένα //try/catch block// της μορφής try { Thread.sleep(4000); } catch (InterruptedException e) { System.out.println("We've been interrupted: no more messages."); return; } Εάν ένα νήμα δεν καλεί κάποια μέθοδο που δημιουργεί [[http://docs.oracle.com/javase/7/docs/api/java/lang/InterruptedException.html|java.lang.InterruptedException]] και η εφαρμογή δουλεύει μέσω κλήσεων της μεθόδου interrupt, θα πρέπει περιοδικά να ελέγχει κατά πόσο υπάρχει κάποια σήμα διακοπής προς το συγκεκριμένο νήμα σε μία ανακύκλωση της μορφής for (int i = 0; i < inputs.length; i++) { heavyCalc(inputs[i]); if (Thread.interrupted()) { throw new InterruptedException(); } } Ο μηχανισμός διακοπής υλοποιείται χρησιμοποιώντας ένα εσωτερικό //flag// (//boolean true/false//) γνωστό ως //interrupt status//. H κλήση της Thread.interrupt() θέτει στην τιμή //true// αυτό το flag. Κάθε φορά που ένα νήμα ελέγχει αν έχει διακοπεί ή όχι (καλώντας τη στατική μέθοδο [[http://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#interrupted--|Thread.interrupted]]), το παραπάνω //flag// απενεργοποιείται λαμβάνοντας την τιμή //false//. Η μη στατική μέθοδος [[http://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#isInterrupted--|isInterrupted]], χρησιμοποιείται από ένα νήμα για να ενημερωθεί για την κατάσταση διακοπής του ιδίου ή ενός άλλου νήματος (εξαρτάται από το αντικείμενο που την καλεί), αλλά δεν αλλάζει την κατάσταση του //flag// διακοπής. ===== Περιμένοντας ένα νήμα να ολοκληρώσει μέσω της μεθόδου join() ===== Η μέθοδος [[http://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#join()|join()]] επιτρέπει σε ένα νήμα να σταματήσει την εκτέλεση του έως ότου ένα άλλο νήμα να ολοκληρώσει την εκτέλεση του. Αν υποθέσουμε ότι ένα νήμα 't' εκτελείται παράλληλα με το τρέχον νήμα, εάν το τρέχον νήμα καλέσει το παρακάτω t.join(); τότε η εκτέλεση του σταματάει έως ότου το νήμα **'t'** να τερματίσει την εκτέλεση του. Υπάρχουν οι εξής παραλλαγές της μεθόδου join * **[[http://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#join--|t.join():]]** περίμενε μέχρι το νήμα **t** να τερματίσει * **[[http://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#join-long-|t.join(long millis):]]** μέχρι το νήμα **t** να τερματίσει ή περίμενε για **millis** milliseconds. * **[[http://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#join-long-int-|t.join(long millis, int nanos):]]** μέχρι το νήμα **t** να τερματίσει ή περίμενε για **millis+nanos/1000000** milliseconds. Όπως και η μέθοδος **sleep()** η μέθοδος **join()** δύναται να διακοπεί μέσω της μεθόδου [[http://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#interrupt--|interrupt]], παράγοντας ένα [[http://docs.oracle.com/javase/7/docs/api/java/lang/InterruptedException.html|InterruptedException()]]. ===== Εφαρμόζοντας τις μεθόδους sleep()/interrupt()/join() σε ένα παράδειγμα ===== Δείτε το παρακάτω παράδειγμα εκτέλεσης δύο νημάτων, το οποίο χρησιμοποιεί τις μεθόδους [[http://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#sleep-long-|sleep]], [[http://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#interrupt--|interrupt]] και [[http://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#join--|join]]. Το κεντρικό νήμα (μέθοδος main) περιμένει το νήμα που δημιουργείται για δεδομένο χρονικό διάστημα. Κάθε 1 δευτερόλεπτο το κεντρικό νήμα επιστρέφει από την μέθοδο **join(1000)** και εξετάζει εάν - ο χρόνος εκτέλεσης του ολοκληρώθηκε και - το παιδί δεν έχει ολοκληρώσει την εκτέλεση του. Ο έλεγχος γίνεται μέσω της μεθόδου [[http://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#isAlive--|isAlive]]. Εάν ισχύουν και οι δύο παραπάνω προϋποθέσεις τότε στέλνει ένα //interrupt// στο παιδί και περιμένει έως ότου αυτό να τερματίσει μέσω της **t.join()**. Στην συνέχεια ολοκληρώνει και το κεντρικό νήμα την εκτέλεση του. public class MessageLoop implements Runnable { public void run() { String importantInfo[] = { "Mares eat oats", "Does eat oats", "Little lambs eat ivy", "A kid will eat ivy too" }; try { for (int i = 0; i < importantInfo.length; i++) { // Pause for 4 seconds Thread.sleep(4000); // Print a message SimpleThreads.threadMessage(importantInfo[i]); } } catch (InterruptedException e) { SimpleThreads.threadMessage("I wasn't done!"); } } } public class SimpleThreads { // Display a message, preceded by // the name of the current thread public static void threadMessage(String message) { String threadName = Thread.currentThread().getName(); System.out.format("%s: %s%n", threadName, message); } public static void main(String args[]) throws InterruptedException { // Delay, in milliseconds before // we interrupt MessageLoop // thread (default one hour). long patience = 1000 * 60 * 60; // If command line argument // present, gives patience // in seconds. if (args.length > 0) { try { patience = Long.parseLong(args[0]) * 1000; } catch (NumberFormatException e) { System.err.println("Argument must be an integer."); System.exit(1); } } threadMessage("Starting MessageLoop thread"); long startTime = System.currentTimeMillis(); Thread t = new Thread(new MessageLoop()); t.start(); threadMessage("Waiting for MessageLoop thread to finish"); // loop until MessageLoop // thread exits while (t.isAlive()) { threadMessage("Still waiting..."); // Wait maximum of 1 second // for MessageLoop thread // to finish. t.join(1000); if (((System.currentTimeMillis() - startTime) > patience) && t.isAlive()) { threadMessage("Tired of waiting!"); t.interrupt(); // Shouldn't be long now // -- wait indefinitely t.join(); } } threadMessage("Finally!"); } } |Προηγούμενο: [[:java:sockets | Sockets ]] | [[:toc | Περιεχόμενα ]] | Επόμενο: [[:java:synchronization| Προβλήματα συγχρονισμού με χρήση νημάτων ]]|