Semaphore
Introduction
Le Semaphore
est un mécanisme de synchronisation qui limite le nombre de
threads qui peuvent accéder simultanément à une ressource partagée.
Contrairement au Lock
qui n'autorise qu'un seul thread à la fois, le
semaphore permet à un nombre défini de threads d'accéder à la ressource.
Concept
Un semaphore maintient un compteur interne qui représente le nombre de "permissions" disponibles :
- Lorsqu'un thread appelle
acquire()
, le compteur diminue - Lorsqu'un thread appelle
release()
, le compteur augmente - Si le compteur atteint zéro, les threads suivants sont bloqués jusqu'à ce qu'une permission soit libérée
Cas d'usage typiques
- Pool de connexions : Limiter le nombre de connexions simultanées à une base de données
- Limitation de bande passante : Contrôler le nombre de téléchargements simultanés
- Gestion de ressources : Limiter l'accès à des imprimantes, fichiers, ou autres ressources physiques
Exemple pratique : Pool de connexions
Voici un exemple simple d'utilisation d'un Semaphore
pour gérer un pool de
connexions limitées.
import threading
import time
import random
class PoolConnexions:
"""Simule un pool de connexions limitées"""
def __init__(self, max_connexions=3):
self.max_connexions = max_connexions
self.semaphore = threading.Semaphore(max_connexions)
self.connexions_actives = 0
self.lock_stats = threading.Lock()
def utiliser_connexion(self, duree_utilisation):
"""Utilise une connexion du pool"""
nom_thread = threading.current_thread().name
print(f"{nom_thread}: Demande de connexion...")
# Acquérir une "connexion" (semaphore) avec context manager
with self.semaphore:
# Incrémenter le compteur de façon thread-safe
with self.lock_stats:
self.connexions_actives += 1
print(f"✅ {nom_thread}: Connexion obtenue ({self.connexions_actives}/{self.max_connexions} utilisées)")
# Simuler l'utilisation de la connexion
time.sleep(duree_utilisation)
print(f"🔄 {nom_thread}: Utilisation terminée")
# Décrémenter le compteur de façon thread-safe
with self.lock_stats:
self.connexions_actives -= 1
print(f"🔓 {nom_thread}: Connexion libérée")
def client_database(pool, client_id):
"""Simule un client qui utilise la base de données"""
for i in range(2):
duree = random.uniform(1, 3)
pool.utiliser_connexion(duree)
# Pause entre les utilisations
time.sleep(random.uniform(0.5, 1))
# Test du pool de connexions
print("=== Test Pool de Connexions avec Semaphore ===")
pool = PoolConnexions(max_connexions=2)
threads = []
for i in range(4): # Plus de clients que de connexions disponibles
thread = threading.Thread(
target=client_database,
args=(pool, i+1),
name=f"Client-{i+1}"
)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print("Tous les clients ont terminé")
Analyse du comportement
Dans cet exemple :
- Semaphore(2) : Seules 2 connexions simultanées sont autorisées
- 4 clients : Plus de demandes que de ressources disponibles
- Gestion automatique : Les threads en excès attendent automatiquement
Sortie attendue
=== Test Pool de Connexions avec Semaphore ===
Client-1: Demande de connexion...
✅ Client-1: Connexion obtenue (1/2 utilisées)
Client-2: Demande de connexion...
✅ Client-2: Connexion obtenue (2/2 utilisées)
Client-3: Demande de connexion...
Client-4: Demande de connexion...
🔄 Client-1: Utilisation terminée
🔓 Client-1: Connexion libérée
✅ Client-3: Connexion obtenue (2/2 utilisées)
...
Semaphore vs Lock
Caractéristique | Lock | Semaphore |
---|---|---|
Accès simultané | 1 thread | N threads configurables |
Usage typique | Protection données critiques | Limitation de ressources |
Blocage | Exclusion mutuelle totale | Limitation contrôlée |