Semaphore: Was es ist, wie es funktioniert und seine Anwendung in der Thread-Synchronisation

Autor: IT Sectr Veröffentlicht: 2026-03-19 Lesezeit: 8 Min.

Semaphore ist ein Synchronisationsprimitiv, das den Zugriff auf eine gemeinsame Ressource über einen Zähler und eine Warteschlange wartender Threads steuert. Laut Wikipedia, 2024 wurde der Semaphor 1965 von Edsger Dijkstra vorgeschlagen, um Probleme der Multithread-Interaktion zu lösen. Das Werkzeug ermöglicht es, die Anzahl der Threads zu begrenzen, die gleichzeitig mit einem kritischen Abschnitt arbeiten.

Wichtigste Erkenntnisse

  • Semaphore ist ein Synchronisationsprimitiv, das den Zugriff über einen Berechtigungszähler steuert.
  • Binärer Semaphor nimmt die Werte 0 und 1 an und fungiert als Blockierungsflag.
  • Zählender Semaphor erlaubt gleichzeitigen Zugriff für eine bestimmte Anzahl von Threads.
  • Im Gegensatz zu einem Mutex ist ein Semaphor nicht an einen Besitzer-Thread gebunden.
  • Deadlock ist eine der Hauptgefahren bei falscher Verwendung von Semaphoren.

Was ist ein Semaphore?

Semaphore ist ein Synchronisationsprimitiv, das einen Zähler verwendet, um den Zugriff auf eine gemeinsame Ressource zu steuern. Das Konzept wurde 1965 von Edsger Dijkstra vorgeschlagen und wurde zur Grundlage aller modernen Synchronisationsmechanismen in Betriebssystemen.

Definition und Zweck

Ein Semaphor ist eine Integer-Variable mit zwei atomaren Operationen: wait (acquire) und signal (release). Die wait-Operation verringert den Zähler, während signal ihn erhöht. Wenn der Zähler Null erreicht, wird der Thread, der wait aufruft, blockiert, bis ein anderer Thread signal ausführt.

Der Hauptzweck eines Semaphors ist der Schutz kritischer Abschnitte vor gleichzeitigem Zugriff durch mehrere Threads. Im Gegensatz zu einem Mutex erfordert ein Semaphor keine Bindung an einen Besitzer-Thread, was ihn für eine breitere Palette von Koordinationsaufgaben geeignet macht.

Geschichte und theoretische Grundlage

Das Konzept des Semaphors entstand im Kontext des Betriebssystems THE, das an der Technischen Hogeschool Eindhoven entwickelt wurde. Dijkstra formalisierte den Semaphor als mathematische Abstraktion und bewies seine Ausreichendheit für die Implementierung beliebiger Synchronisationsprimitive.

Wie funktioniert ein Semaphor?

Der Semaphor-Mechanismus basiert auf zwei atomaren Operationen und einer internen Warteschlange. Wenn acquire aufgerufen wird, überprüft der Thread den Zählerwert und setzt entweder die Ausführung fort oder blockiert, bis die Ressource freigegeben wird.

Zähler und atomare Operationen

Beim Erstellen eines Semaphors wird ein Anfangswert des Berechtigungszählers festgelegt. Jeder acquire-Aufruf verringert den Zähler um 1. Wenn der Zähler danach negativ wird, wird der Thread blockiert. Die release-Operation erhöht den Zähler und weckt einen der wartenden Threads auf.

kotlin
import java.util.concurrent.Semaphore

val semaphore = Semaphore(3)

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

Warteschlange und Planung

Wenn ein Thread acquire mit einem Nullzähler aufruft, platziert das OS ihn in der FIFO-Warteschlange des Semaphors. Der Thread wechselt in den BLOCKED-Zustand und verbraucht keine CPU-Zeit. Nach einem release-Aufruf wechselt der erste Thread in der Warteschlange in den RUNNABLE-Zustand und erhält Zugriff auf die Ressource.

Arten von Semaphoren

In der Synchronisationstheorie werden zwei Hauptarten von Semaphoren unterschieden: binär und zählend. Die Wahl der Art hängt von der spezifischen Aufgabe der Zugriffsverwaltung auf Ressourcen ab.

Binärer Semaphor (Binary Semaphore)

Ein binärer Semaphor nimmt nur die Werte 0 und 1 an. Im Verhalten ähnelt er einem Mutex, jedoch ohne die Besitzeranforderung — jeder Thread kann release ausführen. Solche Semaphore eignen sich gut zur Implementierung von Bereitschaftsflags und Ereignissen zwischen Threads.

kotlin
val ready = Semaphore(0)

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

fun consumer() {
    ready.acquire()
    println("Daten bereit")
}

Zählender Semaphor (Counting Semaphore)

Ein zählender Semaphor kann jeden nicht-negativen Wert annehmen. Er wird verwendet, um einen Pool gleichartiger Ressourcen zu verwalten, bei dem mehrere Instanzen verfügbar sind. Zum Beispiel ein Pool von 5 Netzwerkverbindungen: Jeder acquire nimmt eine Verbindung, release gibt sie an den Pool zurück.

Zählende Semaphore sind unverzichtbar für die Ratenbegrenzung des Zugriffs auf externe Dienste und die Implementierung von Thread-Pools. Sie ermöglichen die präzise Steuerung des Parallelitätsgrads ohne manuelles Thread-Management.

ParameterBinärer SemaphorZählender Semaphor
Bereich0 oder 10 bis N
Gleichzeitige Threads1bis zu N
AnwendungSignalisierung, FlagsRessourcenpools, Ratenbegrenzung

Semaphore vs Mutex

Entwickler verwechseln oft Semaphor und Mutex, obwohl es grundlegende Unterschiede zwischen ihnen gibt. Das Verständnis dieser Unterschiede ist entscheidend für die Wahl des richtigen Synchronisationsmechanismus in einem Projekt.

Besitzerprinzip

Der Hauptunterschied ist das Besitzerkonzept. Ein Mutex weiß immer, welcher Thread ihn erworben hat, und nur dieser Thread kann ihn freigeben. Ein Semaphor hat keinen Besitzer: Jeder Thread kann release aufrufen, ohne acquire aufgerufen zu haben. Dies macht einen Mutex sicherer für den Datenschutz und einen Semaphor flexibler für die Koordination.

Leistung und Anwendungsszenarien

In der Praxis ist ein Mutex dank Optimierungen für typische Szenarien schneller für einfache gegenseitige Ausschließung. Ein Semaphor erfordert zusätzlichen Overhead für die Aufrechterhaltung des Zählers. Für die Begrenzung der Parallelität oder die Implementierung des Erzeuger-Verbraucher-Musters ist jedoch ein Semaphor unverzichtbar.

EigenschaftSemaphoreMutex
Besitzerkein Besitzerhat Besitzer
Freigabejeder Threadnur Besitzer-Thread
Zähler0 bis Nbinär
AnwendungsfallParallelitätsbegrenzung und SignalisierungSchutz kritischer Abschnitte
Rekursionneinja (wiedereintrittsfähig)

Semaphore in der mobilen Entwicklung

In der Entwicklung mobiler Anwendungen wird Semaphore verwendet, um den Zugriff auf begrenzte Ressourcen zu verwalten: Netzwerkverbindungen, Dateien, Datenbanken und Hardware-Komponenten. Moderne Plattformen bieten bequeme eingebaute Implementierungen.

Server-Verbindungspool

Ein typischer Anwendungsfall ist ein HTTP-Verbindungspool. Eine Anwendung kann nicht mehr als 4 gleichzeitige Anfragen an einen Server senden, da die API des Anbieters die Parallelität begrenzt. Ein Semaphor mit einem Anfangswert von 4 stellt sicher, dass die Anzahl der gleichzeitigen Anfragen unter jeder Last das Limit nicht überschreitet, während andere Threads in der Warteschlange warten.

Ohne einen Semaphor könnte ein starker Anstieg der Benutzeraktivität zu einer plötzlichen Überlastung der Serverinfrastruktur führen, was Timeouts und 429 Too Many Requests-Fehler zur Folge hätte. Semaphore fungiert wie eine Sicherung und erlaubt streng eine bestimmte Anzahl gleichzeitiger Aufrufe, unabhängig von der Anzahl aktiver Threads.

Semaphore in Kotlin für Android

Android bietet die Klasse Semaphore aus dem Paket java.util.concurrent. Betrachten wir ein Beispiel zur Begrenzung gleichzeitiger Netzwerkanfragen auf zwei Threads, um eine Serverüberlastung zu verhindern.

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

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

DispatchSemaphore in Swift für iOS

In iOS löst DispatchSemaphore von GCD die gleiche Aufgabe. Entwickler verwenden es, um den Zugriff auf Ressourcen in asynchronem Code zu synchronisieren, ohne den Hauptthread zu blockieren.

swift
let semaphore = DispatchSemaphore(value: 3)

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

Typische Fehler bei der Arbeit mit Semaphoren

Der häufigste Fehler ist ein vergessenes release bei einer Ausnahme. Wenn ein Thread vor dem Aufruf von release mit einem Fehler endet, bleibt der Semaphor für andere Threads dauerhaft blockiert. Verwenden Sie try/finally oder defer zur garantierten Freigabe. Das zweite Problem ist ein Deadlock beim Erwerb mehrerer Semaphore in unterschiedlicher Reihenfolge durch verschiedene Threads.

Semaphore-Verwendungsmuster

Semaphore werden nicht nur zum Datenschutz verwendet, sondern auch zur Thread-Koordination in komplexen Multithread-Szenarien. Die Kenntnis gängiger Muster beschleunigt die Entwicklung und verringert die Wahrscheinlichkeit von Synchronisationsfehlern.

Es gibt mehrere bewährte Muster für die Verwendung von Semaphoren in realen Projekten. Ihre Kenntnis hilft, typische Fehler zu vermeiden und zuverlässige Multithread-Systeme zu erstellen.

Ratenbegrenzer (Rate Limiter)

Ein Semaphor mit einem Anfangswert von N und periodischem release über einen Timer implementiert die Ratenbegrenzung von API-Anfragen. Beispielsweise erlaubt ein Dienst 10 Anfragen pro Sekunde: Der Semaphor startet bei 10, jede Anfrage verringert den Zähler, und ein separater TimerTask setzt den Zähler jede Sekunde auf den Anfangswert zurück. Dies schützt sowohl die Anwendung als auch den Server vor Überlastung.

Erzeuger-Verbraucher mit Semaphoren

Beim klassischen Erzeuger-Verbraucher-Problem verwalten zwei Semaphore einen Puffer: empty (Schreibberechtigungen) und full (Leseberechtigungen). Der Erzeuger ruft acquire auf empty und release auf full auf, während der Verbraucher das Gegenteil tut. Dieses Schema stellt sicher, dass der Verbraucher nie einen leeren Puffer liest und der Erzeuger ihn nie überläuft.

Das gleiche Schema liegt dem begrenzten Puffer in Betriebssystemen zugrunde — einem Ringpuffer mit fester Größe. In mobilen Anwendungen wird das Muster zur Verarbeitung von Warteschlangen für Bilder, Videodateien und Analyseereignisse verwendet.

Throttling von Netzwerkanfragen

Semaphore werden erfolgreich zum Throttling von Netzwerkaufrufen in Hintergrunddiensten eingesetzt. Beispielsweise sendet eine Analyseanwendung Ereignispakete an den Server. Ohne Begrenzung gleichzeitiger Threads bei Spitzenlasten (App-Start, Synchronisation nach Offline-Phasen) kann die Anzahl gleichzeitiger Anfragen die Serverlimits überschreiten. Ein Semaphor mit einem Anfangswert von 3 gewährleistet ein reibungsloses Senden und verhindert serverseitige Blockierungen.

Häufig gestellte Fragen

Was ist der Unterschied zwischen einem Semaphore und einem gewöhnlichen Zähler?

Semaphore ist nicht nur ein Zähler, sondern ein Synchronisationsprimitiv mit atomaren Operationen und einer Warteschlange. Ein gewöhnlicher Zähler blockiert keinen Thread und garantiert keine atomare Inkrementierung bei gleichzeitigem Zugriff mehrerer Threads.

Kann ein Semaphor einen Deadlock verursachen?

Ja, Deadlock ist möglich, wenn mehrere Semaphore in unterschiedlicher Reihenfolge von verschiedenen Threads erworben werden. Beispiel: Thread A erwirbt S1, dann S2, während Thread B S2, dann S1 erwirbt. Legen Sie eine einheitliche Erwerbsreihenfolge für alle Semaphore im Projekt fest.

Was passiert beim Aufruf von acquire mit einem Nullzähler?

Der Thread blockiert und wechselt in den Wartezustand. Er verbraucht keine CPU-Zeit, bis ein anderer Thread release aufruft. In Java ist dies der BLOCKED-Zustand; in Swift wird der Thread von GCD ausgesetzt.

Wie unterscheidet sich ein Binary Semaphore grundlegend von einem Mutex?

Der Hauptunterschied ist der Besitzer. Ein Mutex kann nur vom Besitzer-Thread freigegeben werden. Ein Binary Semaphore kann von jedem Thread freigegeben werden, was für die Signalisierung zwischen Threads praktisch, aber für den Schutz der Datenintegrität weniger sicher ist.

Welchen Anfangswert sollte ich für den Zähler wählen?

Der Anfangswert hängt vom Szenario ab. Zum Schutz einer einzelnen Ressource — 1. Für einen Pool von N Verbindungen — N. Für die Signalisierung zwischen Threads verwenden Sie 0, damit der Verbraucher-Thread auf ein Signal vom Erzeuger wartet.

Zusammenfassung

  • Semaphore ist ein zählerbasiertes Synchronisationsprimitiv, das von Dijkstra im Jahr 1965 vorgeschlagen wurde.
  • Binärer Semaphor nimmt die Werte 0 und 1 an; zählender Semaphor nimmt jeden nicht-negativen Wert an.
  • Acquire- und release-Operationen sind atomar und threadsicher.
  • Im Gegensatz zu einem Mutex ist ein Semaphor nicht an einen Besitzer-Thread gebunden.
  • Zählende Semaphore werden zur Verwaltung von Ressourcenpools und zur Begrenzung der Parallelität verwendet.
  • Vergessenes release ist der häufigste Fehler, der zum Hängen von Threads führt.

Wir entwickeln eine mobile Applikation schlüsselfertig

IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.

Projekt besprechen

Lesen Sie auch