Deadlock (αμοιβαίος αποκλεισμός) — είναι μια κατάσταση κατά την οποία δύο ή περισσότερα νήματα περιμένουν απεριόριστα την απελευθέρωση πόρων που έχουν καταληφθεί από άλλους συμμετέχοντες. Σύμφωνα με το Oracle Java Tutorials (2024), το Deadlock προκύπτει από κυκλική αναμονή, όταν κάθε νήμα κρατάει ένα κλείδωμα που χρειάζεται ένα άλλο νήμα. Χωρίς ειδικά μέσα ανίχνευσης, Deadlock σταματά πλήρως την εκτέλεση της εφαρμογής χωρίς ορατά σφάλματα.
Βασικά σημεία
Deadlock (αμοιβαίος αποκλεισμός) — είναι μια κατάσταση στον πολυνηματικό προγραμματισμό όπου δύο ή περισσότερα νήματα αποκλείουν μόνιμα το ένα το άλλο. Κάθε νήμα κρατάει έναν πόρο που χρειάζεται ένα άλλο νήμα και δεν τον απελευθερώνει, περιμένοντας τη κατάληψη του πόρου που λείπει. Ως αποτέλεσμα, κανένα από τα νήματα δεν μπορεί να συνεχίσει την εκτέλεση.
Στην κινητή ανάπτυξη, το Deadlock είναι ιδιαίτερα κρίσιμο επειδή δεν προκαλεί εξαιρέσεις ή συντριβή. Η εφαρμογή απλώς σταματά να αποκρίνεται σε ενέργειες χρήστη (ANR — Application Not Responding), και η μόνη λύση είναι η βιαισμένη τερματισμός της διεργασίας. Σύμφωνα με τα δεδομένα Google (Android Performance Patterns, 2023), περίπου 15% των αναφορών ANR στο Google Play Console σχετίζονται με αμοιβαίους αποκλεισμούς σε νήματα υποβάθρου.
Η βασική διαφορά του Deadlock από άλλα προβλήματα συναπτροφής — η μη αναστρεψιμότητά του χωρίς εξωτερική παρέμβαση. Τα νήματα δεν θα απελευθερώσουν τους πόρους από μόνα τους, επειδή ο χρονοδρομολογητής του λειτουργικού συστήματος δεν μπορεί να αποσύρει βιαίως το κλείδωμα. Αυτό διαφοροποιεί το Deadlock από Livelock, όπου τα νήματα είναι ενεργά αλλά δεν εκτελούν χρήσιμη εργασία.
Το 1971, ο Edward G. Coffman διατύπωσε τέσσερεις υποχρεωτικές συνθήκες που είναι απαραίτητες για την εμφάνιση Deadlock. Αν ακόμα μια από αυτές λείπει, ο αμοιβαίος αποκλεισμός είναι αδύνατος. Αυτές οι συνθήκες είναι γνωστές ως συνθήκες Coffman και αποτελούν τη βάση όλων των αλγορίθμων πρόληψης Deadlock.
Ένας πόρος μπορεί να καταληφθεί μόνο από ένα νήμα κάθε στιγμή. Εάν ένας πόρος επιτρέπει την ταυτόχρονη ανάγνωση από πολλά νήματα (π.χ. ReadWriteLock σε λειτουργία ανάγνωσης), δεν προκύπτει Deadlock. Αυτή η συνθήκη απορρέαι από την ίδια τη φύση του Mutex και των κλειδωμάτων.
Ένα νήμα κρατάει έναν ήδη κατειλημμένο πόρο και ταυτόχρονα περιμένει την κατάληψη ενός άλλου πόρου. Εάν το νήμα μπορεί να απελευθερώσει τον τρέχοντα πόρο πριν από το αίτημα για τον επόμενο (μέσω διφασικού κλειδώματος), η συνθήκη Hold and Wait παραβιάζεται. Στο Android, αυτό εκδηλώνεται συχνά όταν ένα νήμα κρατάει το κλείδωμα βάσης δεδομένων και προσπαθεί να καταλάβει το κλείδωμα SharedPreferences.
Το λειτουργικό σύστημα δεν μπορεί να αφαιρέσει βιαίως το κλείδωμα από ένα νήμα. Ο πόρος απελευθερώνεται μόνο όταν το νήμα τον απελευθερώσει μόνο του. Σε ορισμένα συστήματα (π.χ. λειτουργία WAL του SQLite), η βιαισμενη προκαταβολή εφαρμόζεται σε επίπεδο μεμονωμένων λειτουργιών, μειώνοντας τον κίνδυνο Deadlock.
Υπάρχει μια κλειστή αλυσίδα νημάτων, κάθε ένα από τα οποία περιμένει έναν πόρο που κρατίεται από το επόμενο στην αλυσίδα. Παράδειγμα, το νήμα A κρατάει τον πόρο 1 και περιμένει τον πόρο 2, το νήμα B κρατάει τον πόρο 2 και περιμένει τον πόρο 1. Αυτή είναι η μόνη συνθήκη που ο προγραμματιστής μπορεί να εξαλείψει αρχιτεκτονικά — μέσω ιεραρχίας κλειδωμάτων. Εάν όλα τα νήματα καταλαμβάνουν πόρους σε μια απολύτως καθορισμένη σειρά, ο κύκλος είναι φυσικά αδύνατος.
Στην πράξη, σε εφαρμογές Android, το Deadlock προκύπτει συχνότερα λόγω υπονόησης διασταύρωσης κλειδωμάτων διαφορετικών επιπέδων: κλείδωμα βάσης δεδομένων (Room), κλείδωμα SharedPreferences και κλείδωμα συλλογής στη μνήμη. Κάθέ ένα από αυτά τα κλείδωματα διαχειρίζεται από διαφορετικά συστατικά, και χωρίς ένα κεντρικό πρωτόκολλο σειράς κατάληψης, οι προγραμματιστές δημιουργούν ακούσια κύκλους.
Ας δούμε ένα κλασσικό παράδειγμα αμοιβαίου αποκλεισμού — δύο νήματα καταλαμβάνουν κλειδώματα σε διαφορετική σειρά. Εάν το πρώτο νήμα αποκλείει τον πόρο A και προσπαθεί να καταλάβει τον B, και το δεύτερο αποκλείει τον B και προσπαθεί να καταλάβει τον A, προκύπτει Deadlock.
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun operationA() {
synchronized(lockA) {
Thread.sleep(50) // προσομοίωση λειτουργίας
synchronized(lockB) {
println("operationA εκτελέστηκε")
}
}
}
fun operationB() {
synchronized(lockB) { // αντίστροφη σειρά!
Thread.sleep(50)
synchronized(lockA) {
println("operationB εκτελέστηκε")
}
}
}
}
fun main() {
val ex = DeadlockExample()
Thread { ex.operationA() }.start()
Thread { ex.operationB() }.start()
// Η εφαρμογή θα κολλήσει για πάντα — Deadlock!
}
Σε αυτό το παράδειγμα, η operationA καταλαμβάνει το lockA, και η operationB καταλαμβάνει το lockB. Στη συνέχεια, κάθε μία προσπαθεί να καταλάβει το δεύτερο κλείδωμα — και οι δύο περιμένουν απεριόριστα. Το πρόγραμμα κολλάει χωρίς μήνυμα σφάλματος. Ο μόνος τρόπος διόρθωσης είναι η εγγύηση της ίδιας σειράς κατάληψης κλειδωμάτων σε όλες τις μεθόδους.
Αυτά τα τρία προβλήματα συναπτροφής συχνά συγχέονται, αλλά οι μηχανισμοί και οι συνέπειές τους είναι εντελώς διαφορετικές. Deadlock — πλήρης σταματημός, Starvation — απεριόριστη αναμονή για έναν πόρο, Livelock — ενεργή αδράνεια. Η κατανόηση των διαφορών είναι κρίσιμη για την επιλογή της σωστής στρατηγικής εξάλειψης.
Thread Dump (απορριψή νημάτων) — το κύριο εργαλείο για την ανίχνευση αμοιβαίων αποκλεισμών σε JVM και Android Runtime. Κατά την απορριψή, το JVM αναλύει αυτόματα το γραφήματικα αναπτύξεων ανάμεσα σε μονήρες και επισημαίνει τους κύκλους Deadlock.
Αυτόματη ανίχνευση Deadlock κατά τη διάρκεια εκτέλεσης επιτυγχάνεται μέσω χρονομετρητών Watchdog. Εάν ένα νήμα δεν ολοκληρώσει την λειτουργία εντός του καθορισμένου χρονικού περιορισμού, ο watchdog ξεκινά τη δημιουργία απορριψής και στέλνει αναφορά στο σύστημα αναφοράς σφαλμάτων (Firebase Crashlytics, Sentry).
Ο πιο αξιόπιστος τρόπος — η θέσπιση μιας συνολικής σειράς κατάληψης κλειδωμάτων σε όλη την εφαρμογή.
TryLock — μέθοδος κλειδώματος που δεν αποκλείει το νήμα απεριόριστα, αλλά επιστρέφει false εάν το κλείδωμα δεν μπορεί να αποκτηθεί εντός καθορισμένου χρόνου.
Ο αλγόριθμος τραπεζίτη — είναι μια θεωρητική μέθοδος πρόληψης Deadlock, που προτάθηκε από τον Edsger Dijkstra.
Συχνές Ερωτήσεις
Όχι, για τον αμοιβαίο αποκλεισμό χρειάζονται τουλάχιστον δύο νήματα.
Στις κορουτίνες, το Deadlock προκύπτει σε επίπεδο ανασταλμένων συναρτήσεων (suspend) και δεν αποκλείει το νήμα OS.
Deadlock στο SQLite προκύπτει όταν δύο συνδέσεις βάσης δεδομένων προσπαθούν να εκτελέσουν συναλλαγές σε διαφορετική σειρά.
Android Runtime διαθετει ενσωματωμένο ανιχνευτή Deadlock που ενεργοποιείται κατά τη δημιουργία ANR.
Πρώτα, λάβετε Thread Dump όλων των νημάτων της εφαρμογής.
Συμπερασμα
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης