Το Semaphore είναι ένα πρωτόγονο συγχρονισμού που διαχειρίζεται την πρόσβαση σε έναν κοινόχρηστο πόρο μέσω ενός μετρητή και μιας ουράς νημάτων που περιμένουν. Σύμφωνα με το Wikipedia, 2024, το semaphore προτάθηκε από τον Edsger Dijkstra το 1965 για την επίλυση προβλημάτων πολυνηματικής αλληλεπίδρασης. Το εργαλείο επιτρέπει τον περιορισμό του αριθμού των νημάτων που εργάζονται ταυτόχρονα με μια κρίσιμη ενότητα.
Κύρια σημεία
Semaphore — είναι ένα πρωτόγονο συγχρονισμού που χρησιμοποιεί έναν μετρητή για τη διαχείριση της πρόσβασης σε έναν κοινόχρηστο πόρο. Η ιδέα προτάθηκε από τον Edsger Dijkstra το 1965 και έγινε το θεμέλιο όλων των σύγχρονων μηχανισμών συγχρονισμού στα λειτουργικά συστήματα.
Το semaphore είναι μια ακέραια μεταβλητή με δύο ατομικές λειτουργίες: wait (acquire) και signal (release). Η λειτουργία wait μειώνει τον μετρητή και η signal τον αυξάνει. Όταν ο μετρητής φτάσει στο μηδέν, το νήμα που καλεί wait μπλοκάρεται μέχρι να εκτελεστεί signal από άλλο νήμα.
Ο κύριος σκοπός του semaphore είναι η προστασία κρίσιμων ενοτήτων από ταυτόχρονη πρόσβαση πολλαπλών νημάτων. Σε αντίθεση με το mutex, το semaphore δεν απαιτεί δέσμευση στο νήμα-ιδιοκτήτη, καθιστώντας το κατάλληλο για ευρύτερο φάσμα εργασιών συντονισμού.
Η ιδέα του semaphore προέκυψε στο πλαίσιο του λειτουργικού συστήματος THE, που αναπτύχθηκε στο Technische Hogeschool Eindhoven. Ο Dijkstra επισημοποίησε το semaphore ως μαθηματική αφαίρεση, αποδεικνύοντας την επάρκειά του για την υλοποίηση οποιωνδήποτε πρωτόγονων συγχρονισμού.
Ο μηχανισμός του semaphore βασίζεται σε δύο ατομικές λειτουργίες και μια εσωτερική ουρά αναμονής. Κατά την κλήση acquire, το νήμα ελέγχει την τιμή του μετρητή και είτε συνεχίζει την εκτέλεση είτε μπλοκάρεται μέχρι να ελευθερωθεί ο πόρος.
Κατά τη δημιουργία του semaphore, ορίζεται η αρχική τιμή του μετρητή αδειών. Κάθε κλήση acquire μειώνει τον μετρητή κατά 1. Εάν μετά από αυτό ο μετρητής γίνει αρνητικός, το νήμα μπλοκάρεται. Η λειτουργία release αυξάνει τον μετρητή και αφυπνίζει ένα από τα νήματα που περιμένουν.
import java.util.concurrent.Semaphore
val semaphore = Semaphore(3)
fun accessResource() {
semaphore.acquire()
try {
println("${Thread.currentThread().name} λειτουργεί")
} finally {
semaphore.release()
}
}
Όταν ένα νήμα καλεί acquire με μηδενικό μετρητή, το ΛΣ το τοποθετεί στην ουρά FIFO του semaphore. Το νήμα μεταβαίνει σε κατάσταση BLOCKED, χωρίς να καταναλώνει χρόνο επεξεργαστή. Μετά την κλήση release, το πρώτο νήμα στην ουρά μεταβαίνει σε κατάσταση RUNNABLE και αποκτά πρόσβαση στον πόρο.
Στη θεωρία συγχρονισμού διακρίνονται δύο κύριοι τύποι semaphores: δυαδικό (binary) και αριθμητικό (counting). Η επιλογή τύπου εξαρτάται από τη συγκεκριμένη εργασία διαχείρισης πρόσβασης σε πόρους.
Το δυαδικό semaphore λαμβάνει μόνο τιμές 0 και 1. Στη συμπεριφορά του μοιάζει με mutex, αλλά χωρίς απαίτηση ιδιοκτησίας — οποιοδήποτε νήμα μπορεί να εκτελέσει release. Τέτοια semaphores είναι βολικά για την υλοποίηση σημαιών ετοιμότητας και γεγονότων μεταξύ νημάτων.
val ready = Semaphore(0)
fun producer() {
Thread.sleep(1000)
ready.release()
}
fun consumer() {
ready.acquire()
println("Τα δεδομένα είναι έτοιμα")
}
Αριθμητικό semaphore μπορεί να λάβει οποιαδήποτε μη αρνητική τιμή. Χρησιμοποιείται για τη διαχείριση μιας δεξαμενής ομοιογενών πόρων όπου πολλά στιγμιότυπα είναι διαθέσιμα. Για παράδειγμα, μια δεξαμενή 5 συνδέσεων δικτύου: κάθε acquire καταλαμβάνει μία σύνδεση, η release την επιστρέφει στη δεξαμενή.
Τα αριθμητικά semaphores είναι απαραίτητα για τον περιορισμό της ταχύτητας πρόσβασης σε εξωτερικές υπηρεσίες και την υλοποίηση δεξαμενών νημάτων. Επιτρέπουν τον ακριβή έλεγχο του βαθμού παραλληλισμού χωρίς χειροκίνητη διαχείριση νημάτων.
| Παράμετρος | Δυαδικό semaphore | Αριθμητικό semaphore |
|---|---|---|
| Εύρος | 0 ή 1 | από 0 έως N |
| Νήματα ταυτόχρονα | 1 | έως N |
| Εφαρμογή | σηματοδότηση, σημαίες | δεξαμενές πόρων, rate limiting |
Οι προγραμματιστές συχνά μπερδεύουν το semaphore και το mutex, αν και υπάρχουν θεμελιώδεις διαφορές μεταξύ τους. Η κατανόηση αυτών των διαφορών είναι κρίσιμη για την επιλογή του σωστού μηχανισμού συγχρονισμού σε ένα έργο.
Η βασική διαφορά — η έννοια της ιδιοκτησίας. Το mutex γνωρίζει πάντα ποιο νήμα το κατέλαβε και μόνο αυτό το νήμα μπορεί να το ελευθερώσει. Το semaphore δεν έχει ιδιοκτήτη: οποιοδήποτε νήμα μπορεί να καλέσει release, ακόμη και χωρίς να καλέσει acquire. Αυτό καθιστά το mutex ασφαλέστερο για προστασία δεδομένων και το semaphore πιο ευέλικτο για συντονισμό.
Στην πράξη, το mutex είναι ταχύτερο για απλό αμοιβαίο αποκλεισμό χάρη στις βελτιστοποιήσεις για το τυπικό σενάριο. Το semaphore απαιτεί πρόσθετο κόστος για τη διατήρηση του μετρητή. Ωστόσο, για τον περιορισμό του παραλληλισμού ή την υλοποίηση του προτύπου “παραγωγός-καταναλωτής”, το semaphore είναι απαραίτητο.
| Χαρακτηριστικό | Semaphore | Mutex |
|---|---|---|
| Ιδιοκτησία | χωρίς ιδιοκτήτη | έχει ιδιοκτήτη |
| Απελευθέρωση | οποιοδήποτε νήμα | μόνο νήμα-ιδιοκτήτης |
| Μετρητής | από 0 έως N | δυαδικός |
| Χρήση | περιορισμός παραλληλισμού και σηματοδότηση | προστασία κρίσιμης ενότητας |
| Αναδρομή | όχι | ναι (reentrant) |
Στην ανάπτυξη εφαρμογών κινητών, το Semaphore χρησιμοποιείται για τη διαχείριση πρόσβασης σε περιορισμένους πόρους: συνδέσεις δικτύου, αρχεία, βάσεις δεδομένων και εξαρτήματα υλικού. Οι σύγχρονες πλατφόρμες παρέχουν βολικές ενσωματωμένες υλοποιήσεις.
Μία από τις τυπικές περιπτώσεις — δεξαμενή συνδέσεων HTTP. Η εφαρμογή μπορεί να στέλνει ταυτόχρονα το πολύ 4 αιτήματα στον διακομιστή, επειδή το API του παρόχου περιορίζει τον παραλληλισμό. Ένα semaphore με αρχική τιμή 4 εγγυάται ότι σε οποιοδήποτε φορτίο ο αριθμός των ταυτόχρονων αιτημάτων δεν θα υπερβεί το όριο και τα υπόλοιπα νήματα θα περιμένουν στην ουρά.
Χωρίς semaphore, κατά την απότομη αύξηση της δραστηριότητας των χρηστών, η υποδομή του διακομιστή μπορεί να υποστεί ξαφνική υπερφόρτωση, οδηγώντας σε χρονικές υπερβάσεις και σφάλματα 429 Too Many Requests. Το Semaphore λειτουργεί σαν ασφάλεια, επιτρέποντας αυστηρά καθορισμένο αριθμό ταυτόχρονων κλήσεων ανεξάρτητα από τον αριθμό των ενεργών νημάτων.
Το Android παρέχει την κλάση Semaphore από το πακέτο java.util.concurrent. Ας εξετάσουμε ένα παράδειγμα περιορισμού των ταυτόχρονων αιτημάτων δικτύου σε δύο νήματα για την αποφυγή υπερφόρτωσης του διακομιστή.
class ApiClient {
private val throttle = Semaphore(2)
suspend fun fetch(url: String): Result {
throttle.acquire()
return try {
httpGet(url)
} finally {
throttle.release()
}
}
}
Στο iOS, το DispatchSemaphore από το GCD λύνει το ίδιο πρόβλημα. Οι προγραμματιστές το χρησιμοποιούν για συγχρονισμό πρόσβασης σε πόρους σε ασύγχρονο κώδικα χωρίς αποκλεισμό του κύριου νήματος.
let semaphore = DispatchSemaphore(value: 3)
func processBatch(_ items: [UIImage]) {
for img in items {
semaphore.wait()
DispatchQueue.global().async {
applyFilter(to: img)
semaphore.signal()
}
}
}
Το πιο συνηθισμένο λάθος — ξεχασμένο release σε περίπτωση εξαίρεσης. Εάν ένα νήμα τερματιστεί με σφάλμα πριν καλέσει release, το semaphore παραμένει για πάντα αποκλεισμένο για τα άλλα νήματα. Χρησιμοποιήστε try/finally ή defer για εγγυημένη απελευθέρωση. Το δεύτερο πρόβλημα είναι το deadlock κατά τη λήψη πολλαπλών semaphores σε διαφορετική σειρά από διαφορετικά νήματα.
Τα semaphores χρησιμοποιούνται όχι μόνο για προστασία δεδομένων, αλλά και για συντονισμό νημάτων σε σύνθετα πολυνηματικά σενάρια. Η γνώση κοινών προτύπων επιταχύνει την ανάπτυξη και μειώνει την πιθανότητα σφαλμάτων συγχρονισμού.
Υπάρχουν πολλά αποδεδειγμένα πρότυπα εφαρμογής semaphores σε πραγματικά έργα. Η γνώση τους βοηθά στην αποφυγή τυπικών λαθών και στην κατασκευή αξιόπιστων πολυνηματικών συστημάτων.
Ένα semaphore με αρχική τιμή N και περιοδική release μέσω χρονοδιακόπτη υλοποιεί περιορισμό ταχύτητας αιτημάτων προς το API. Για παράδειγμα, η υπηρεσία επιτρέπει 10 αιτήματα ανά δευτερόλεπτο: το semaphore ξεκινά με 10, κάθε αίτημα μειώνει τον μετρητή και ένα ξεχωριστό TimerTask επαναφέρει τον μετρητή στην αρχική τιμή μία φορά ανά δευτερόλεπτο. Αυτό προστατεύει τόσο την εφαρμογή όσο και τον διακομιστή από υπερφόρτωση.
Στην κλασική εργασία παραγωγός-καταναλωτής, δύο semaphores διαχειρίζονται τον buffer: empty (άδεια εγγραφής) και full (άδεια ανάγνωσης). Ο παραγωγός καλεί acquire στο empty και release στο full, ο καταναλωτής — αντίστροφα. Αυτό το σχήμα εγγυάται ότι ο καταναλωτής ποτέ δεν θα διαβάσει άδειο buffer και ο παραγωγός δεν θα τον υπεργεμίσει.
Το ίδιο σχήμα βρίσκεται στη βάση του περιορισμένου buffer στα λειτουργικά συστήματα — του κυκλικού buffer σταθερού μεγέθους. Σε εφαρμογές κινητών, αυτό το πρότυπο χρησιμοποιείται για την επεξεργασία ουρών εικόνων, αρχείων βίντεο και αναλυτικών γεγονότων.
Τα semaphores χρησιμοποιούνται με επιτυχία για throttling κλήσεων δικτύου σε υπηρεσίες παρασκηνίου. Για παράδειγμα, μια εφαρμογή αναλυτικής στέλνει πακέτα γεγονότων στον διακομιστή. Χωρίς περιορισμό των ταυτόχρονων νημάτων, σε φορτία αιχμής (εκκίνηση εφαρμογής, συγχρονισμός μετά από offline) ο αριθμός των ταυτόχρονων αιτημάτων μπορεί να υπερβεί τα όρια του διακομιστή. Ένα semaphore με αρχική τιμή 3 εγγυάται ομαλή αποστολή και αποτρέπει τον αποκλεισμό στην πλευρά του διακομιστή.
Συχνές Ερωτήσεις
Το semaphore δεν είναι απλός μετρητής, αλλά πρωτόγονο συγχρονισμού με ατομικές λειτουργίες και ουρά αναμονής. Ο συνηθισμένος μετρητής δεν μπλοκάρει το νήμα και δεν εγγυάται την ατομικότητα της αύξησης κατά την ταυτόχρονη πρόσβαση πολλαπλών νημάτων.
Ναι, deadlock είναι δυνατόν κατά τη λήψη πολλαπλών semaphores σε διαφορετική σειρά από διαφορετικά νήματα. Για παράδειγμα, το νήμα A λαμβάνει S1, στη συνέχεια S2, και το νήμα B — S2, στη συνέχεια S1. Καθορίστε μια ενιαία σειρά λήψης για όλα τα semaphores στο έργο.
Το νήμα μπλοκάρεται και μεταβαίνει σε κατάσταση αναμονής. Δεν καταναλώνει χρόνο επεξεργαστή μέχρι ένα άλλο νήμα να καλέσει release. Στην Java αυτή είναι η κατάσταση BLOCKED, στο Swift το νήμα αναστέλλεται από το GCD.
Η κύρια διαφορά — ιδιοκτησία. Το Mutex μπορεί να απελευθερωθεί μόνο από το νήμα-ιδιοκτήτη. Το Binary Semaphore μπορεί να απελευθερωθεί από οποιοδήποτε νήμα, που είναι βολικό για σηματοδότηση μεταξύ νημάτων, αλλά λιγότερο ασφαλές για προστασία της ακεραιότητας δεδομένων.
Η αρχική τιμή εξαρτάται από το σενάριο. Για προστασία ενός πόρου — 1. Για δεξαμενή N συνδέσεων — N. Για σηματοδότηση μεταξύ νημάτων χρησιμοποιήστε 0, ώστε το νήμα-καταναλωτής να περιμένει σήμα από τον παραγωγό.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης