Semaphore: co to jest, zasada działania i zastosowanie w synchronizacji wątków

Autor: IT Sectr Opublikowano: 2026-03-19 Czas czytania: 8 min

Semaphore to prymityw synchronizacji, który zarządza dostępem do współdzielonego zasobu za pomocą licznika i kolejki oczekujących wątków. Według Wikipedia, 2024, semafor został zaproponowany przez Edsgera Dijkstrę w 1965 roku do rozwiązywania problemów wielowątkowej interakcji. Narzędzie pozwala ograniczyć liczbę wątków jednocześnie pracujących z sekcją krytyczną.

Najważniejsze

  • Semaphore — prymityw synchronizacji zarządzający dostępem za pomocą licznika zezwoleń.
  • Semafor binarny przyjmuje wartości 0 i 1, działając jak flaga blokady.
  • Semafor licznikowy dopuszcza jednoczesny dostęp określonej liczby wątków.
  • W przeciwieństwie do mutexu, semafor nie jest przypisany do wątku-właściciela.
  • Deadlock — jedno z głównych zagrożeń przy nieprawidłowym użyciu semaforów.

Co to jest Semaphore?

Semaphore — to prymityw synchronizacji wykorzystujący licznik do zarządzania dostępem do współdzielonego zasobu. Koncepcja została zaproponowana przez Edsgera Dijkstrę w 1965 roku i stała się fundamentem wszystkich nowoczesnych mechanizmów synchronizacji w systemach operacyjnych.

Definicja i przeznaczenie

Semafor to zmienna całkowitoliczbowa z dwoma atomowymi operacjami: wait (acquire) i signal (release). Operacja wait zmniejsza licznik, a signal zwiększa go. Gdy licznik osiąga zero, wątek wywołujący wait zostaje zablokowany do momentu wykonania signal przez inny wątek.

Głównym przeznaczeniem semafora jest ochrona sekcji krytycznych przed jednoczesnym dostępem wielu wątków. W przeciwieństwie do mutexu, semafor nie wymaga przypisania do wątku-właściciela, co czyni go odpowiednim do szerszego zakresu zadań koordynacyjnych.

Historia i podstawa teoretyczna

Koncepcja semafora powstała w kontekście systemu operacyjnego THE, opracowanego w Technische Hogeschool Eindhoven. Dijkstra sformalizował semafor jako abstrakcję matematyczną, udowadniając jego wystarczalność do implementacji dowolnych prymitywów synchronizacji.

Jak działa semafor?

Mechanizm semafora opiera się na dwóch atomowych operacjach i wewnętrznej kolejce oczekiwania. Podczas wywołania acquire wątek sprawdza wartość licznika i albo kontynuuje wykonanie, albo zostaje zablokowany do momentu zwolnienia zasobu.

Licznik i operacje atomowe

Podczas tworzenia semafora ustawiana jest początkowa wartość licznika zezwoleń. Każde wywołanie acquire zmniejsza licznik o 1. Jeśli po tym licznik stanie się ujemny, wątek zostaje zablokowany. Operacja release zwiększa licznik i budzi jeden z oczekujących wątków.

kotlin
import java.util.concurrent.Semaphore

val semaphore = Semaphore(3)

fun accessResource() {
    semaphore.acquire()
    try {
        println("${Thread.currentThread().name} pracuje")
    } finally {
        semaphore.release()
    }
}

Kolejka oczekiwania i planowanie

Gdy wątek wywołuje acquire przy zerowym liczniku, system operacyjny umieszcza go w kolejce FIFO semafora. Wątek przechodzi w stan BLOCKED, nie zużywając czasu procesora. Po wywołaniu release pierwszy wątek w kolejce przechodzi do stanu RUNNABLE i uzyskuje dostęp do zasobu.

Rodzaje semaforów

W teorii synchronizacji wyróżnia się dwa główne typy semaforów: binarny (binary) i licznikowy (counting). Wybór typu zależy od konkretnego zadania zarządzania dostępem do zasobów.

Semafor binarny (Binary Semaphore)

Semafor binarny przyjmuje tylko wartości 0 i 1. Swoim zachowaniem przypomina mutex, ale bez wymogu posiadania — każdy wątek może wykonać release. Takie semafory są wygodne do implementacji flag gotowości i zdarzeń między wątkami.

kotlin
val ready = Semaphore(0)

fun producer() {
    Thread.sleep(1000)
    ready.release()
}

fun consumer() {
    ready.acquire()
    println("Dane gotowe")
}

Semafor licznikowy (Counting Semaphore)

Semafor licznikowy może przyjmować dowolną nieujemną wartość. Jest używany do zarządzania pulą jednorodnych zasobów, gdzie dostępnych jest kilka instancji. Na przykład pula 5 połączeń sieciowych: każde acquire zajmuje jedno połączenie, release zwraca je do puli.

Semafory licznikowe są niezastąpione przy ograniczaniu prędkości dostępu do zewnętrznych usług i implementacji pul wątków. Pozwalają precyzyjnie kontrolować stopień równoległości bez ręcznego zarządzania wątkami.

ParametrSemafor binarnySemafor licznikowy
Zakres0 lub 1od 0 do N
Wątków jednocześnie1do N
Zastosowaniesygnalizacja, flagipule zasobów, rate limiting

Semaphore vs Mutex

Programiści często mylą semafor i mutex, chociaż istnieją między nimi fundamentalne różnice. Zrozumienie tych różnic jest kluczowe dla wyboru odpowiedniego mechanizmu synchronizacji w projekcie.

Zasada posiadania

Kluczowa różnica — koncepcja posiadania. Mutex zawsze wie, który wątek go przechwycił, i tylko ten wątek może go zwolnić. Semafor nie ma właściciela: każdy wątek może wywołać release, nawet nie wywołując acquire. To czyni mutex bezpieczniejszym do ochrony danych, a semafor — bardziej elastycznym do koordynacji.

Wydajność i scenariusze użycia

W praktyce mutex jest szybszy dla prostej blokady wzajemnej dzięki optymalizacjom typowego scenariusza. Semafor wymaga dodatkowych narzutów na utrzymanie licznika. Jednak do ograniczania równoległości lub implementacji wzorca „producent-konsument“ semafor jest niezastąpiony.

CechaSemaphoreMutex
Posiadaniebrak właścicielajest właściciel
Zwolnieniekażdy wątektylko wątek-właściciel
Licznikod 0 do Nbinarny
Use caseograniczanie równoległości i sygnalizacjaochrona sekcji krytycznej
Rekursjanietak (reentrant)

Zastosowanie Semaphore w programowaniu mobilnym

W tworzeniu aplikacji mobilnych Semaphore jest używany do zarządzania dostępem do ograniczonych zasobów: połączeń sieciowych, plików, baz danych i komponentów sprzętowych. Nowoczesne platformy udostępniają wygodne wbudowane implementacje.

Pula połączeń z serwerem

Jednym z typowych przypadków jest pula połączeń HTTP. Aplikacja może jednocześnie wysyłać nie więcej niż 4 zapytania do serwera, ponieważ API dostawcy ogranicza równoległość. Semafor z początkową wartością 4 gwarantuje, że przy dowolnym obciążeniu liczba jednoczesnych zapytań nie przekroczy limitu, a pozostałe wątki będą czekać w kolejce.

Bez semafora przy gwałtownym wzroście aktywności użytkowników infrastruktura serwerowa może doznać nagłego przeciążenia, co prowadzi do timeoutów i błędów 429 Too Many Requests. Semafor działa jak bezpiecznik, przepuszczając ściśle określoną liczbę jednoczesnych wywołań niezależnie od liczby aktywnych wątków.

Semaphore w Kotlin dla Androida

Android udostępnia klasę Semaphore z pakietu java.util.concurrent. Rozważmy przykład ograniczenia jednoczesnych zapytań sieciowych do dwóch wątków w celu zapobieżenia przeciążeniu serwera.

kotlin
class ApiClient {
    private val throttle = Semaphore(2)

    suspend fun fetch(url: String): Result {
        throttle.acquire()
        return try {
            httpGet(url)
        } finally {
            throttle.release()
        }
    }
}

DispatchSemaphore w Swift dla iOS

W iOS DispatchSemaphore z GCD rozwiązuje to samo zadanie. Programiści używają go do synchronizacji dostępu do zasobów w kodzie asynchronicznym bez blokowania głównego wątku.

swift
let semaphore = DispatchSemaphore(value: 3)

func processBatch(_ items: [UIImage]) {
    for img in items {
        semaphore.wait()
        DispatchQueue.global().async {
            applyFilter(to: img)
            semaphore.signal()
        }
    }
}

Typowe błędy przy pracy z semaforami

Najczęstszym błędem jest zapomniany release przy wyjątku. Jeśli wątek kończy się z błędem przed wywołaniem release, semafor na zawsze pozostaje zablokowany dla pozostałych wątków. Używaj try/finally lub defer do gwarantowanego zwolnienia. Drugi problem to deadlock przy przechwytywaniu kilku semaforów w różnej kolejności przez różne wątki.

Wzorce użycia semaforów

Semafory są stosowane nie tylko do ochrony danych, ale również do koordynacji wątków w złożonych scenariuszach wielowątkowych. Znajomość powszechnych wzorców przyspiesza rozwój i zmniejsza prawdopodobieństwo błędów synchronizacji.

Istnieje kilka sprawdzonych wzorców stosowania semaforów w rzeczywistych projektach. Ich znajomość pomaga uniknąć typowych błędów i budować niezawodne systemy wielowątkowe.

Rate Limiter (ograniczanie prędkości)

Semafor z początkową wartością N i okresowym release przez timer implementuje ograniczanie prędkości zapytań do API. Na przykład usługa zezwala na 10 zapytań na sekundę: semafor startuje z 10, każde zapytanie zmniejsza licznik, a osobny TimerTask raz na sekundę przywraca licznik do wartości początkowej. To chroni zarówno aplikację, jak i serwer przed przeciążeniem.

Producent-Konsument przez semafory

W klasycznym zadaniu producent-konsument dwa semafory zarządzają buforem: empty (zezwolenia na zapis) i full (zezwolenia na odczyt). Producent wywołuje acquire na empty i release na full, Konsument — odwrotnie. Taki schemat gwarantuje, że Konsument nigdy nie odczyta pustego bufora, a Producent go nie przepełni.

Ten sam schemat leży u podstaw bufora ograniczonego w systemach operacyjnych — bufora cyklicznego o stałym rozmiarze. W aplikacjach mobilnych wzorzec jest stosowany do przetwarzania kolejek obrazów, plików wideo i zdarzeń analitycznych.

Throttling zapytań sieciowych

Semafory są z powodzeniem używane do tłumienia wywołań sieciowych w usługach działających w tle. Na przykład aplikacja analityczna wysyła do serwera pakiety zdarzeń. Bez ograniczenia liczby współbieżnych wątków przy szczytowym obciążeniu (uruchomienie aplikacji, synchronizacja po offline) liczba jednoczesnych zapytań może przekroczyć limity serwera. Semafor z początkową wartością 3 gwarantuje płynne wysyłanie i zapobiega blokadzie po stronie serwera.

Często zadawane pytania

Jaka jest różnica między Semaphore a zwykłym licznikiem?

Semafor to nie tylko licznik, ale prymityw synchronizacji z atomowymi operacjami i kolejką oczekiwania. Zwykły licznik nie blokuje wątku i nie gwarantuje atomowości inkrementacji przy współbieżnym dostępie wielu wątków.

Czy semafor może spowodować deadlock?

Tak, deadlock jest możliwy przy przechwytywaniu kilku semaforów w różnej kolejności przez różne wątki. Na przykład wątek A przechwytuje S1, potem S2, a wątek B — S2, potem S1. Ustal jednolitą kolejność przechwytywania dla wszystkich semaforów w projekcie.

Co się dzieje przy acquire z zerowym licznikiem?

Wątek zostaje zablokowany i przechodzi w stan oczekiwania. Nie zużywa czasu procesora do momentu, aż inny wątek wywoła release. W Javie jest to stan BLOCKED, w Swift wątek jest wstrzymywany przez GCD.

Czym Binary Semaphore zasadniczo różni się od Mutex?

Główna różnica — posiadanie. Mutex może być zwolniony tylko przez wątek-właściciela. Binary Semaphore może być zwolniony przez dowolny wątek, co jest wygodne do sygnalizacji między wątkami, ale mniej bezpieczne do ochrony integralności danych.

Jaką początkową wartość wybrać dla licznika?

Początkowa wartość zależy od scenariusza. Do ochrony jednego zasobu — 1. Dla puli N połączeń — N. Do sygnalizacji między wątkami używaj 0, aby wątek-konsument czekał na sygnał od producenta.

Podsumowanie

  • Semaphore — prymityw synchronizacji oparty na liczniku, zaproponowany przez Dijkstrę w 1965 roku.
  • Semafor binarny przyjmuje wartości 0 i 1, licznikowy — dowolną nieujemną wartość.
  • Operacje acquire i release są atomowe i bezpieczne wątkowo.
  • W przeciwieństwie do mutexu, semafor nie jest przypisany do wątku-właściciela.
  • Semafory licznikowe są stosowane do zarządzania pulami zasobów i ograniczania równoległości.
  • Zapomniany release — najczęstszy błąd prowadzący do zawieszenia wątków.

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również