1. Introduction
Mutex et lock — c'est comme un barista qui sert un seul client à la fois. Mais si on n'a pas une seule machine à café, mais trois — et qu'on peut préparer trois tasses en même temps ?
Par exemple, vous avez un café avec trois machines à café. Les clients (threads) arrivent, prennent une machine libre, préparent leur café et partent. Si les trois machines sont occupées, les autres attendent qu'une se libère.
Question : Comment faire pour que pas plus de trois clients travaillent aux machines en même temps, et que les autres attendent leur tour ?
Réponse : utiliser un sémaphore !
Qu'est-ce qu'un sémaphore ?
Le sémaphore est un outil classique de synchronisation. Si lock/Mutex contrôlent "un est passé — les autres attendent", alors le sémaphore dit : "je permets N en même temps !".
Les sémaphores ont été proposés par Edsger Dijkstra en 1965. Le nom vient de la signalisation maritime : comme des drapeaux transmettaient des informations disponibles, le sémaphore dans le code informe les threads — vous pouvez entrer ou devez attendre.
Scénarios d'utilisation
- Limiter le nombre de threads travaillant simultanément avec une ressource.
- Limiter les connexions simultanées à une DB, le nombre de requêtes parallèles, les tâches lourdes.
2. Aperçu des classes : Semaphore et SemaphoreSlim
Semaphore
- Classe lourde, utilise des objets noyau OS (kernel objects).
- Supporte la synchronisation entre threads de différents processus.
- On peut définir un nom et partager entre processus.
SemaphoreSlim
- Version allégée, fonctionne seulement à l'intérieur d'un seul processus.
- Plus rapide et plus économe en ressources.
- Presque toujours préféré si la synchronisation inter-processus n'est pas nécessaire.
Analogie : un sac à dos de randonnée (SemaphoreSlim) contre une grosse valise (Semaphore). Si tu voyages léger — prends le sac à dos.
Table comparative
| Classe | Inter-processus | Performance | Recommandé |
|---|---|---|---|
|
Oui | Plus lent | Quand la synchronisation entre processus est nécessaire |
|
Non | Plus rapide | Dans 99% des cas, à l'intérieur d'un seul processus |
Méthodes et propriétés principales du sémaphore
Paramètres principaux
- InitialCount — le nombre initial d'autorisations.
- MaxCount — le maximum d'autorisations distribuées simultanément.
Méthodes clés
- Wait() ou WaitAsync() — demander l'accès (prendre une autorisation).
- Release() — libérer une autorisation.
Comment ça marche
Si lors de l'appel à Wait() il n'y a pas d'autorisations, le thread est bloqué et attend que quelqu'un appelle Release(). Après une libération, l'un des threads en attente reprendra l'exécution.
3. Premier exemple pratique
Ajoutons à une application console un "parking" de 3 places et essayons de lancer 10 threads.
using System;
using System.Threading;
class Program
{
// Semaphore avec 3 autorisations (3 places de parking)
static SemaphoreSlim parking = new SemaphoreSlim(3);
static void Main()
{
for (int i = 1; i <= 10; i++)
{
int carNumber = i;
new Thread(() =>
{
Console.WriteLine($"Voiture #{carNumber} essaie de se garer...");
parking.Wait(); // Attend une place libre
Console.WriteLine($"Voiture #{carNumber} est entrée dans le parking !");
Thread.Sleep(2000); // On reste 2 secondes sur le parking
Console.WriteLine($"Voiture #{carNumber} quitte le parking.");
parking.Release(); // On libère la place
}).Start();
}
}
}
- Seulement trois voitures se "gareront" en même temps.
- Les autres attendront qu'une place se libère.
- L'affichage sera mélangé — c'est normal en multithreading.
4. Le sémaphore comme limiteur de charge
Limitons le nombre de tâches lourdes exécutées simultanément (par ex. des téléchargements) à 5.
static SemaphoreSlim semaphore = new SemaphoreSlim(5); // max 5 téléchargements simultanés
static void DownloadFile(int fileId)
{
semaphore.Wait();
try
{
Console.WriteLine($"--> Je commence le téléchargement du fichier {fileId}");
Thread.Sleep(1000 + fileId * 100); // Téléchargement (simulation)
Console.WriteLine($"<-- Fichier {fileId} téléchargé");
}
finally
{
semaphore.Release();
}
}
static void Main()
{
for (int i = 1; i <= 12; i++)
{
int localId = i;
new Thread(() => DownloadFile(localId)).Start();
}
}
Point important : on place Wait() avant le bloc try, et Release() dans le finally. Ainsi l'autorisation sera bien libérée même en cas d'exception.
5. Wait(int millisecondsTimeout) et méthodes asynchrones
On peut attendre seulement pendant un temps limité :
if (semaphore.Wait(500))
{
// On a réussi à prendre l'autorisation en une demi-seconde !
}
else
{
// En 500 ms on n'a pas attendu — on abandonne
}
Dans les applications modernes (par ex. ASP.NET) utilisez la variante asynchrone : await semaphore.WaitAsync(). Ça ne bloque pas le thread d'exécution pendant l'attente de l'autorisation.
Remarque : dans du code asynchrone utilisez précisément SemaphoreSlim et son WaitAsync, sinon vous pouvez obtenir des deadlocks inattendus.
6. Exemples de mauvais et bon usage
Erreur fréquente — oublier d'appeler Release() : les autorisations "fuient" et tout s'arrête.
Mauvais
static void SomeWork()
{
semaphore.Wait();
// ... traitement, mais Release oublié !
}
Bon
static void SomeWork()
{
semaphore.Wait();
try
{
// traitement
}
finally
{
semaphore.Release();
}
}
Variante asynchrone
static async Task SomeAsyncWork()
{
await semaphore.WaitAsync();
try
{
// traitement asynchrone
}
finally
{
semaphore.Release();
}
}
7. Structure interne du sémaphore (explication "simple")
Le sémaphore est un compteur. Wait() le décrémente de 1. S'il était > 0 — le thread passe ; s'il est à 0 — le thread attend. Release() incrémente le compteur et réveille les threads en attente.
+-------------------------------+
| Sémaphore (compteur = 3) |
+-------------------------------+
| [ ] [ ] [ ] | <--- Autorisations
+----+----+----+----------------+
| | |
Thread Thread Thread
8. Nuances utiles
Différences avec d'autres primitives
- lock / Monitor / Mutex — laissent passer un seul thread (accès exclusif).
- Semaphore/SemaphoreSlim — laissent passer un nombre limité de N threads simultanément.
Le sémaphore n'est pas lié à un "propriétaire" : une autorisation peut être libérée par n'importe quel thread. C'est une caractéristique, pas un bug.
Application dans la vraie vie
- Limiter les connexions parallèles à un service ou une DB.
- Pool : pas plus de N threads sur la ressource.
- Limiter le nombre de requêtes web traitées simultanément.
- Limiter lecture/écriture pour éviter la surcharge.
- Limiter les appels à un API externe.
Exemple d'erreur (Release plus que Wait)
var semaphore = new SemaphoreSlim(2);
semaphore.Release(); // Erreur ! Le compteur devient 3, dépassant MaxCount — une SemaphoreFullException sera lancée.
Ici on aura une SemaphoreFullException : le compteur a dépassé le maximum.
Différences entre Semaphore et SemaphoreSlim
- SemaphoreSlim — intra-processus, plus rapide et plus simple (à utiliser presque toujours).
- Semaphore — nécessaire pour la synchronisation inter-processus (scénario rare).
Pourquoi connaître les sémaphores ?
Question classique en entretien : "Comment limiter le nombre de threads travaillant avec une ressource ?" — la bonne réponse : sémaphore.
- lock — 1 thread.
- Semaphore/SemaphoreSlim — N threads.
9. Erreurs typiques et particularités d'utilisation des sémaphores
Erreur n°1 : on oublie d'appeler Release(). Si un thread a pris une autorisation (Wait() ou WaitAsync()) mais ne la libère pas, les autres attendront indéfiniment — l'application "gèlera".
Erreur n°2 : on appelle Release() plus de fois qu'il n'y a eu de Wait(). Des autorisations "en trop" apparaissent. Pour Semaphore cela conduira à une SemaphoreFullException et une logique d'accès cassée.
Erreur n°3 : on mélange différents mécanismes de synchronisation. À un endroit — lock, ailleurs — sémaphore sur la même ressource. Ça augmente le risque de deadlocks.
Erreur n°4 : on utilise Semaphore dans du code asynchrone. Le sémaphore classique n'est pas ami avec async/await. Pour les scénarios asynchrones utilisez SemaphoreSlim et WaitAsync().
Erreur n°5 : on définit mal initialCount et maxCount. Avec de mauvaises valeurs, la limitation peut être contournée et plus de threads que prévu accéderont à la ressource.
GO TO FULL VERSION