CodeGym /Kursy /C# SELF /Semyfory: Semaphore...

Semyfory: Semaphore i SemaphoreSlim

C# SELF
Poziom 56 , Lekcja 3
Dostępny

1. Wprowadzenie

Mutex i lock — to jak barista, który obsługuje jednego klienta na raz. Ale co jeśli nie mamy jednej ekspresu, tylko trzy — i jednocześnie można zrobić trzy kawy?

Na przykład masz kawiarnię z trzema ekspresami. Klienci (wątki) przychodzą, zajmują wolny ekspres, robią kawę i wychodzą. Jeśli wszystkie trzy maszyny są zajęte, pozostali czekają, aż zwolni się przynajmniej jedna.

Pytanie: Jak sprawić, żeby jednocześnie przy ekspresach pracowało nie więcej niż trzech klientów, a reszta czekała w kolejce?
Odpowiedź: użyć semafora!

Co to jest semafor?

Semafor — klasyczne narzędzie synchronizacji. Jeśli lock/Mutex kontrolują "jeden wszedł — reszta czeka", to semafor mówi: "pozwalam na N jednocześnie!".

Semafory zostały zaproponowane przez Edsgera Dijkstrę w 1965 roku. Nazwa pochodzi z sygnalizacji morskiej: tak jak flagi przekazywały informacje, tak semafor w kodzie mówi wątkom — możesz wejść albo musisz poczekać.

Scenariusze użycia

  • Ograniczenie liczby wątków pracujących jednocześnie z zasobem.
  • Limit jednoczesnych połączeń do BD, liczby równoległych zapytań, ciężkich zadań.

2. Przegląd klas: Semaphore i SemaphoreSlim

Semaphore

  • Ciężka klasa, używa obiektów jądra OS (kernel objects).
  • Wspiera synchronizację między wątkami różnych procesów.
  • Można podać nazwę i dzielić między procesami.

SemaphoreSlim

  • Odchudzona wersja, działa tylko w ramach jednego procesu.
  • Szybsza i oszczędniejsza jeśli chodzi o zasoby.
  • Prawie zawsze preferowana, jeśli nie potrzebujesz synchronizacji międzyprocesowej.

Analogia: plecak turystyczny (SemaphoreSlim) kontra duża walizka (Semaphore). Podróżujesz lekko — weź plecak.

Tabela porównawcza

Klasa Międzyprocesowy Wydajność Polecane
Semaphore
Tak Wolniejszy Gdy potrzebna synchronizacja między procesami
SemaphoreSlim
Nie Szybszy W 99% przypadków, wewnątrz jednego procesu

Główne metody i właściwości semafora

Główne parametry

  • InitialCount — początkowa liczba zezwoleń.
  • MaxCount — maksimum jednocześnie wydanych zezwoleń.

Kluczowe metody

  • Wait() lub WaitAsync() — zażądać dostępu (zająć zezwolenie).
  • Release() — zwolnić zezwolenie.

Jak to działa
Jeśli przy wywołaniu Wait() nie ma zezwoleń, wątek zostaje zablokowany i czeka, aż ktoś wywoła Release(). Po zwolnieniu jedno z oczekujących wznowi wykonanie.

3. Pierwszy praktyczny przykład

Dodamy do aplikacji konsolowej "parking" na 3 miejsca i spróbujemy uruchomić 10 wątków.

using System;
using System.Threading;

class Program
{
    // Semafor z 3 zezwoleniami (3 miejsca na parkingu)
    static SemaphoreSlim parking = new SemaphoreSlim(3);

    static void Main()
    {
        for (int i = 1; i <= 10; i++)
        {
            int carNumber = i;
            new Thread(() =>
            {
                Console.WriteLine($"Maszyna #{carNumber} próbuje zaparkować...");
                parking.Wait(); // Czeka na wolne miejsce
                Console.WriteLine($"Maszyna #{carNumber} wjechała na parking!");
                Thread.Sleep(2000); // Stoimy na parkingu 2 sekundy
                Console.WriteLine($"Maszyna #{carNumber} wyjeżdża z parkingu.");
                parking.Release(); // Zwalamy miejsce
            }).Start();
        }
    }
}
  • Jednocześnie "zaparkują" tylko trzy maszyny.
  • Pozostali będą czekać na zwolnienie miejsca.
  • Wyjście konsoli będzie pomieszane — to normalne przy wielowątkowości.

4. Semafor jako ogranicznik obciążenia

Ograniczmy liczbę jednocześnie wykonywanych ciężkich zadań (np. pobrań) do 5.

static SemaphoreSlim semaphore = new SemaphoreSlim(5); // maksymalnie 5 jednoczesnych pobrań

static void DownloadFile(int fileId)
{
    semaphore.Wait();
    try
    {
        Console.WriteLine($"--> Zaczynam pobieranie pliku {fileId}");
        Thread.Sleep(1000 + fileId * 100); // Pobieranie (symulacja)
        Console.WriteLine($"<-- Plik {fileId} pobrany");
    }
    finally
    {
        semaphore.Release();
    }
}

static void Main()
{
    for (int i = 1; i <= 12; i++)
    {
        int localId = i;
        new Thread(() => DownloadFile(localId)).Start();
    }
}

Ważna uwaga: Wait() umieszczamy przed blokiem try, a Release() — w finally. Dzięki temu zezwolenie na pewno zostanie zwolnione nawet w przypadku wyjątku.

5. Wait(int millisecondsTimeout) i metody asynchroniczne

Można czekać tylko ograniczony czas:

if (semaphore.Wait(500))
{
    // Udało się zająć zezwolenie w pół sekundy!
}
else
{
    // W 500 ms nie doczekaliśmy się — przerwane
}

W nowoczesnych aplikacjach (np. ASP.NET) używaj wariantu asynchronicznego: await semaphore.WaitAsync(). To nie blokuje wątku wykonania podczas oczekiwania na zezwolenie.
Uwaga: w kodzie asynchronicznym używaj właśnie SemaphoreSlim i jego WaitAsync, inaczej możesz dostać niespodziewane deadlock-i.

6. Przykłady złej i dobrej praktyki

Częsty błąd — zapomnieć wywołać Release(): zezwolenia "uciekają" i wszystko staje.

Źle

static void SomeWork()
{
    semaphore.Wait();
    // ... przetwarzanie, a Release zapomniano!
}

Dobrze

static void SomeWork()
{
    semaphore.Wait();
    try
    {
        // przetwarzanie
    }
    finally
    {
        semaphore.Release();
    }
}

Wersja asynchroniczna

static async Task SomeAsyncWork()
{
    await semaphore.WaitAsync();
    try
    {
        // asynchroniczne przetwarzanie
    }
    finally
    {
        semaphore.Release();
    }
}

7. Wewnętrzna budowa semafora (wyjaśnienie "na palcach")

Semafor — to licznik. Wait() zmniejsza go o 1. Jeśli był > 0 — wątek przechodzi; jeśli 0 — wątek czeka. Release() zwiększa licznik i budzi oczekujących.


+-------------------------------+
| Semafor (licznik = 3)         |
+-------------------------------+
|  [ ]  [ ]  [ ]                | <--- Zezwolenia
+----+----+----+----------------+
     |    |    |
   Wątek Wątek Wątek

8. Przydatne niuanse

Różnice w porównaniu z innymi prymitywami

  • lock / Monitor / Mutex — wpuszczają tylko jeden wątek (dostęp ekskluzywny).
  • Semaphore/SemaphoreSlim — wpuszczają ograniczoną liczbę N wątków jednocześnie.

Semafor nie jest powiązany z "właścicielem": zezwolenie może zwolnić dowolny wątek. To cecha, nie błąd.

Zastosowanie w praktyce

  • Limit równoległych połączeń do serwisu lub BD.
  • Pula: nie więcej niż N wątków przy zasobie.
  • Ograniczenie jednocześnie obsługiwanych żądań webowych.
  • Limit odczytu/zapisu by chronić przed przeciążeniem.
  • Ograniczenie wywołań zewnętrznego API.

Przykład błędu (Release więcej niż Wait)

var semaphore = new SemaphoreSlim(2);
semaphore.Release(); // Błąd! Licznik stał się 3, przekraczając MaxCount — zostanie rzucony SemaphoreFullException.

Tutaj będzie SemaphoreFullException: licznik przekroczył maksimum.

Różnice między Semaphore i SemaphoreSlim

  • SemaphoreSlim — w ramach procesu, szybszy i prostszy (używaj prawie zawsze).
  • Semaphore — potrzebny do synchronizacji międzyprocesowej (rzadki scenariusz).

Po co znać semafory?

Klasyczne pytanie na rozmowie: "Jak ograniczyć liczbę wątków pracujących z zasobem?" — poprawna odpowiedź: semafor.

  • lock — 1 wątek.
  • Semaphore/SemaphoreSlimN wątków.

9. Typowe błędy i cechy używania semaforów

Błąd nr 1: zapominają wywołać Release(). Jeśli wątek zajął zezwolenie (Wait() lub WaitAsync()), ale go nie zwolnił, pozostali będą czekać w nieskończoność — aplikacja "zamarznie".

Błąd nr 2: wywołują Release() więcej razy niż było Wait(). Pojawiają się "nadmiarowe" zezwolenia. Dla Semaphore to doprowadzi do SemaphoreFullException i popsutej logiki dostępu.

Błąd nr 3: mieszają różne mechanizmy synchronizacji. W jednym miejscu — lock, w innym — semafor dla tego samego zasobu. To zwiększa ryzyko wzajemnych blokad (deadlock).

Błąd nr 4: używają Semaphore w kodzie asynchronicznym. Klasyczny semafor nie jest przyjazny dla async/await. Dla scenariuszy asynchronicznych używaj SemaphoreSlim i WaitAsync().

Błąd nr 5: źle ustawiają initialCount i maxCount. Przy nieprawidłowym doborze wartości ograniczenie można obejść i do zasobu przejdzie więcej wątków niż zaplanowano.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION