Semaphore : ce que c'est, comment ça fonctionne et son utilisation dans la synchronisation de threads

Auteur : IT Sectr Publié le : 2026-03-19 Temps de lecture : 8 min

Semaphore est un primitif de synchronisation qui contrôle l'accès à une ressource partagée via un compteur et une file d'attente de threads en attente. Selon Wikipedia, 2024, le sémaphore a été proposé par Edsger Dijkstra en 1965 pour résoudre les problèmes d'interaction multithread. L'outil permet de limiter le nombre de threads travaillant simultanément avec une section critique.

Points clés

  • Semaphore est un primitif de synchronisation qui contrôle l'accès via un compteur de permissions.
  • Le sémaphore binaire prend les valeurs 0 et 1, fonctionnant comme un drapeau de blocage.
  • Le sémaphore compteur permet l'accès simultané d'un nombre spécifié de threads.
  • Contrairement à un mutex, un sémaphore n'est pas lié à un thread propriétaire.
  • Deadlock est l'un des principaux dangers lors d'une utilisation incorrecte des sémaphores.

Qu'est-ce qu'un Semaphore ?

Semaphore est un primitif de synchronisation qui utilise un compteur pour contrôler l'accès à une ressource partagée. Le concept a été proposé par Edsger Dijkstra en 1965 et est devenu le fondement de tous les mécanismes modernes de synchronisation dans les systèmes d'exploitation.

Définition et objectif

Un sémaphore est une variable entière avec deux opérations atomiques : wait (acquire) et signal (release). L'opération wait diminue le compteur, tandis que signal l'augmente. Lorsque le compteur atteint zéro, le thread qui appelle wait est bloqué jusqu'à ce qu'un autre thread exécute signal.

L'objectif principal d'un sémaphore est de protéger les sections critiques de l'accès simultané par plusieurs threads. Contrairement à un mutex, un sémaphore ne nécessite pas de liaison à un thread propriétaire, ce qui le rend adapté à une gamme plus large de tâches de coordination.

Histoire et fondement théorique

Le concept du sémaphore est né dans le contexte du système d'exploitation THE, développé à la Technische Hogeschool Eindhoven. Dijkstra a formalisé le sémaphore comme une abstraction mathématique, prouvant sa suffisance pour implémenter n'importe quel primitif de synchronisation.

Comment fonctionne un sémaphore ?

Le mécanisme du sémaphore est basé sur deux opérations atomiques et une file d'attente interne. Lorsque acquire est appelé, le thread vérifie la valeur du compteur et soit continue l'exécution, soit se bloque jusqu'à ce que la ressource soit libérée.

Compteur et opérations atomiques

Lors de la création d'un sémaphore, une valeur initiale du compteur de permissions est définie. Chaque appel acquire diminue le compteur de 1. Si le compteur devient négatif après cela, le thread est bloqué. L'opération release augmente le compteur et réveille l'un des threads en attente.

kotlin
import java.util.concurrent.Semaphore

val semaphore = Semaphore(3)

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

File d'attente et ordonnancement

Lorsqu'un thread appelle acquire avec un compteur à zéro, l'OS le place dans la file FIFO du sémaphore. Le thread passe à l'état BLOCKED, ne consommant pas de temps CPU. Après un appel release, le premier thread dans la file passe à l'état RUNNABLE et obtient l'accès à la ressource.

Types de sémaphores

Dans la théorie de la synchronisation, on distingue deux types principaux de sémaphores : binaire et compteur. Le choix du type dépend de la tâche spécifique de gestion d'accès aux ressources.

Sémaphore binaire (Binary Semaphore)

Un sémaphore binaire ne prend que les valeurs 0 et 1. Par son comportement, il ressemble à un mutex, mais sans l'exigence de propriété — n'importe quel thread peut exécuter release. Ces sémaphores sont pratiques pour implémenter des drapeaux de disponibilité et des événements entre threads.

kotlin
val ready = Semaphore(0)

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

fun consumer() {
    ready.acquire()
    println("Données prêtes")
}

Sémaphore compteur (Counting Semaphore)

Un sémaphore compteur peut prendre n'importe quelle valeur non négative. Il est utilisé pour gérer un ensemble de ressources similaires où plusieurs instances sont disponibles. Par exemple, un pool de 5 connexions réseau : chaque acquire prend une connexion, release la retourne au pool.

Les sémaphores compteurs sont indispensables pour limiter la vitesse d'accès aux services externes et implémenter des pools de threads. Ils permettent de contrôler précisément le degré de parallélisme sans gestion manuelle des threads.

ParamètreSémaphore binaireSémaphore compteur
Plage0 ou 1de 0 à N
Threads simultanés1jusqu'à N
Applicationsignalisation, drapeauxpools de ressources, limitation de débit

Semaphore vs Mutex

Les développeurs confondent souvent sémaphore et mutex, bien qu'il existe des différences fondamentales entre eux. Comprendre ces différences est crucial pour choisir le mécanisme de synchronisation approprié dans un projet.

Principe de propriété

La différence clé est le concept de propriété. Un mutex sait toujours quel thread l'a acquis, et seul ce thread peut le libérer. Un sémaphore n'a pas de propriétaire : n'importe quel thread peut appeler release sans même avoir appelé acquire. Cela rend le mutex plus sûr pour la protection des données et le sémaphore plus flexible pour la coordination.

Performances et cas d'utilisation

En pratique, un mutex est plus rapide pour l'exclusion mutuelle simple grâce aux optimisations pour les scénarios typiques. Un sémaphore nécessite une surcharge supplémentaire pour maintenir le compteur. Cependant, pour limiter le parallélisme ou implémenter le modèle producteur-consommateur, un sémaphore est indispensable.

CaractéristiqueSemaphoreMutex
Propriétépas de propriétairea un propriétaire
Libérationn'importe quel threadseul le thread propriétaire
Compteurde 0 à Nbinaire
Cas d'utilisationlimitation du parallélisme et signalisationprotection de section critique
Récursionnonoui (réentrant)

Semaphore dans le développement mobile

Dans le développement d'applications mobiles, Semaphore est utilisé pour gérer l'accès à des ressources limitées : connexions réseau, fichiers, bases de données et composants matériels. Les plateformes modernes offrent des implémentations intégrées pratiques.

Pool de connexions serveur

Un cas d'utilisation typique est un pool de connexions HTTP. Une application ne peut pas envoyer plus de 4 requêtes simultanées à un serveur car l'API du fournisseur limite le parallélisme. Un sémaphore avec une valeur initiale de 4 garantit que sous n'importe quelle charge, le nombre de requêtes concurrentes ne dépasse pas la limite, tandis que les autres threads attendent dans la file.

Sans sémaphore, une augmentation brusque de l'activité utilisateur pourrait provoquer une surcharge soudaine de l'infrastructure serveur, entraînant des timeouts et des erreurs 429 Too Many Requests. Semaphore agit comme un fusible, permettant strictement un nombre spécifié d'appels concurrents indépendamment du nombre de threads actifs.

Semaphore en Kotlin pour Android

Android fournit la classe Semaphore du package java.util.concurrent. Voyons un exemple de limitation des requêtes réseau concurrentes à deux threads pour éviter la surcharge du serveur.

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

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

DispatchSemaphore en Swift pour iOS

Sous iOS, DispatchSemaphore de GCD résout la même tâche. Les développeurs l'utilisent pour synchroniser l'accès aux ressources dans du code asynchrone sans bloquer le thread principal.

swift
let semaphore = DispatchSemaphore(value: 3)

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

Erreurs typiques lors du travail avec les sémaphores

L'erreur la plus courante est un release oublié lorsqu'une exception se produit. Si un thread se termine avec une erreur avant d'appeler release, le sémaphore reste bloqué en permanence pour les autres threads. Utilisez try/finally ou defer pour garantir la libération. Le deuxième problème est le deadlock lors de l'acquisition de plusieurs sémaphores dans un ordre différent par des threads différents.

Modèles d'utilisation des sémaphores

Les sémaphores ne sont pas utilisés seulement pour la protection des données, mais aussi pour la coordination des threads dans des scénarios multithread complexes. Connaître les modèles courants accélère le développement et réduit la probabilité d'erreurs de synchronisation.

Il existe plusieurs modèles éprouvés d'utilisation des sémaphores dans des projets réels. Les connaître aide à éviter les erreurs typiques et à construire des systèmes multithread fiables.

limiteur de débit (Rate Limiter)

Un sémaphore avec une valeur initiale N et une libération périodique via un timer implémente la limitation de débit des requêtes API. Par exemple, un service permet 10 requêtes par seconde : le sémaphore commence à 10, chaque requête diminue le compteur, et un TimerTask séparé ramène le compteur à sa valeur initiale chaque seconde. Cela protège à la fois l'application et le serveur de la surcharge.

Producteur-Consommateur avec sémaphores

Dans le problème classique du producteur-consommateur, deux sémaphores gèrent un tampon : empty (permissions d'écriture) et full (permissions de lecture). Le producteur appelle acquire sur empty et release sur full, tandis que le consommateur fait l'inverse. Ce schéma garantit que le consommateur ne lit jamais un tampon vide et que le producteur ne le dépasse jamais.

Ce même schéma sous-tend le tampon borné dans les systèmes d'exploitation — un tampon circulaire de taille fixe. Dans les applications mobiles, le modèle est utilisé pour traiter les files d'images, de fichiers vidéo et d'événements analytiques.

Limitation des requêtes réseau

Les sémaphores sont utilisés avec succès pour limiter les appels réseau dans les services en arrière-plan. Par exemple, une application d'analyse envoie des paquets d'événements au serveur. Sans limiter les threads concurrents lors des pics de charge (lancement de l'application, synchronisation après hors ligne), le nombre de requêtes simultanées peut dépasser les limites du serveur. Un sémaphore avec une valeur initiale de 3 garantit un envoi fluide et empêche le blocage côté serveur.

Foire aux questions

Quelle est la différence entre un Semaphore et un compteur ordinaire ?

Semaphore n'est pas simplement un compteur, mais un primitif de synchronisation avec des opérations atomiques et une file d'attente. Un compteur ordinaire ne bloque pas un thread et ne garantit pas l'atomicité de l'incrémentation lors d'un accès concurrent par plusieurs threads.

Un sémaphore peut-il causer un deadlock ?

Oui, un deadlock est possible lors de l'acquisition de plusieurs sémaphores dans un ordre différent par des threads différents. Par exemple, le thread A acquiert S1, puis S2, tandis que le thread B acquiert S2, puis S1. Fixez un ordre d'acquisition unique pour tous les sémaphores du projet.

Que se passe-t-il lors d'un appel acquire avec un compteur à zéro ?

Le thread se bloque et entre dans un état d'attente. Il ne consomme pas de temps CPU jusqu'à ce qu'un autre thread appelle release. En Java, c'est l'état BLOCKED ; en Swift, le thread est suspendu par GCD.

En quoi un Binary Semaphore diffère-t-il fondamentalement d'un Mutex ?

La principale différence est la propriété. Un Mutex ne peut être libéré que par le thread propriétaire. Un Binary Semaphore peut être libéré par n'importe quel thread, ce qui est pratique pour la signalisation entre threads mais moins sûr pour protéger l'intégrité des données.

Quelle valeur initiale choisir pour le compteur ?

La valeur initiale dépend du scénario. Pour protéger une seule ressource — 1. Pour un pool de N connexions — N. Pour la signalisation entre threads, utilisez 0 afin que le thread consommateur attende un signal du producteur.

Résumé

  • Semaphore est un primitif de synchronisation basé sur un compteur proposé par Dijkstra en 1965.
  • Le sémaphore binaire prend les valeurs 0 et 1 ; le sémaphore compteur prend n'importe quelle valeur non négative.
  • Les opérations acquire et release sont atomiques et thread-safe.
  • Contrairement à un mutex, un sémaphore n'est pas lié à un thread propriétaire.
  • Les sémaphores compteurs sont utilisés pour gérer des pools de ressources et limiter le parallélisme.
  • Release oublié est l'erreur la plus courante entraînant le blocage des threads.

Nous développerons une application mobile clé en main

IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.

Discuter du projet

Lisez aussi