Semafor w c#

Dziś kolejny post o synchronizacji w c#. Semafor to bardzo popularna forma limitowania liczby wątków, które mogą mieć dostęp do danego kodu. Nie chcę omawiać tutaj podstaw semafora, ale myślę, że warto przypomnieć ogólną zasadę. Pseudokod (źródło Wikipedia):

procedure V (S : Semaphore);
 begin
   (* Operacja atomowa: inkrementacja semafora *)
   S := S + 1;
 end;
 
   (* Operacja atomowa: dekrementacja semafora *)
 procedure P (S : Semaphore);
 begin
   (* Cała operacja jest atomowa *)
   repeat
     Wait();
   until S > 0;
   S := S - 1;
 end;

Semafor składa się z dwóch metod. Jedna zwiększa pewien licznik, druga z kolei zmniejsza. Wejście do sekcji krytycznej jest możliwe tylko wtedy gdy licznik jest większy od zera.  Zatem jeśli na początku licznik ma wartość 1 i zostanie wywołana metoda P to licznik będzie miał wartość 0 a tym samym następnie wywołanie P będzie musiało poczekać. Innymi słowy, przed wejściem zmniejszamy licznik a a przy wyjściu z sekcji krytycznej  zwiększamy go z powrotem. Jeśli początkowa wartość licznika to 1 wtedy tylko jeden wątek może wejść do takiej sekcji. Semafor może dopuścić wiele wątków jednocześnie -  to od nas zależy początkowa wartość licznika.

W .NET semafor jest realizowany za pomocą jednej z dwóch klas. Pierwszą z nich jest Semaphore:

internal class Program
{
   private static int _counter;

   private static void Main(string[] args)
   {
       for (int i = 0; i < 10000; i++)
       {
           var task = new Task(IncreaseValue);
           task.Start();
       }
       Thread.Sleep(2000);
       Console.WriteLine(_counter);

   }
   private static readonly Semaphore _semaphore=new Semaphore(1,1);

   private static void IncreaseValue()
   {
       _semaphore.WaitOne();
       _counter++;
       _semaphore.Release();
   }
}

Za pomocą konstruktora można określić ile wątków chcemy dopuścić do danej sekcji ( w naszym przypadku jest to jeden wątek). WaitOne zmniejsza wspomniany licznik z kolei release zwiększa. Po szczegóły API odsyłam do MSDN. W poście chciałem jednak pokazać drugą klasę odpowiedzialną za semafor: SemaphoreSlim:

internal class Program
{
    private static int _counter;
    
    private static void Main(string[] args)
    {
        for (int i = 0; i < 10000; i++)
        {
            var task = new Task(IncreaseValue);
            task.Start();
        }
        Thread.Sleep(2000);
        Console.WriteLine(_counter);
    }
    private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
    
    private static void IncreaseValue()
    {
        _semaphore.Wait();
        _counter++;
        _semaphore.Release();
    }
}

Jak widać, sposób użycia jest bardzo podobny. Istnieje jednak kilka bardzo ważnych szczegółów:

  1. Używaj SemaphoreSlim dla krótkich operacji.
  2. Semaphore może służyć do synchronizacji wątków miedzy procesami. Wtedy należy skorzystać z tzw. named semaphores.  W takiej sytuacji warto zwrócić szczególną uwagę na bezpieczeństwo aby złośliwe oprogramowanie nie spowodowało deadlock.
  3. SemaphoreSlim to odchudzona wersja przeznaczona do synchronizacji wątków znajdujących się w tym samym procesie. Semaphore z kolei to wrapper na systemowy, niezarządzany semafor, który jest potężniejszy ale znacząco wolniejszy.

Leave a Reply

Your email address will not be published.