Różnice między Mutex a Semaphore

W .NET do synchronizacji dostępnych jest wiele mechanizmów. W dzisiejszym wpisie chciałbym przedstawić różnice między semaforem a muteksem. Na pierwszy rzut oka, wyglądają one identycznie. Przykład:

class Program
{
   static void Main(string[] args)
   {
       Mutex mutex = new Mutex();
       
       mutex.WaitOne();
       Console.WriteLine("Sekcja krytyczna tutaj");
       mutex.ReleaseMutex();

       Semaphore semaphore=new Semaphore(1,1);
       semaphore.WaitOne();
       Console.WriteLine("Sekcja krytyczna tutaj");
       semaphore.Release();
   }
}

Pierwsza różnica to fakt, że semafor może dopuścić kilka wątków naraz. W konstruktorze podajemy ile wątków może uzyskać dostęp jednocześnie do danej sekcji. Mutex jest zawsze binarny i dopuszcza wyłącznie jeden wątek.

Jaka jest różnica między semaforem binarnym a muteksem?

Muteks jest bardziej zaawansowanym tworem. Przede wszystkim zapamiętuje, z którego wątku została założona blokada. Jeśli wątek A wywołał WaitOne, to tylko wątek A może wykonać Release. Semafor tego nie robi, spójrzmy na przykład potwierdzający tezę:

class Program
{
   static Mutex _mutex = new Mutex();

   static void Main(string[] args)
   {
       var t1=Task.Factory.StartNew(Run1);
       Thread.Sleep(100);
       var t2=Task.Factory.StartNew(Run2);

       Task.WaitAll(t1, t2);            
   }
   static void Run1()
   {
       _mutex.WaitOne();
   }
   static void Run2()
   {
       _mutex.ReleaseMutex();
   }
}

Powyższy kod wyrzuci wyjątek ponieważ nie można ReleaseMutex wywołać z innego wątku niż ten, w którym został wykonany WaitOne. Mutex przechowuje zatem identyfikator wątku. Dla odmiany, poniższy kod, korzystający z semafora, nie spowoduje wyjątku:

class Program
{
   static Semaphore _semaphore=new Semaphore(1,1);

   static void Main(string[] args)
   {
       var t1=Task.Factory.StartNew(Run1);
       Thread.Sleep(100);
       var t2=Task.Factory.StartNew(Run2);

       Task.WaitAll(t1, t2);            
   }
   static void Run1()
   {
       _semaphore.WaitOne();
   }
   static void Run2()
   {
       _semaphore.Release();
   }
}

Kolejną zmianą jest rekurencja. Przyjrzyjmy się poniższemu kodu, który próbuje założyć blokadę dwa razy ale w tym samym wątku:

class Program
{
   static Semaphore _semaphore=new Semaphore(1,1);

   static void Main(string[] args)
   {
       var t1=Task.Factory.StartNew(Run1);
       Task.WaitAll(t1);            
   }
   static void Run1()
   {
       _semaphore.WaitOne();
       Console.WriteLine("Run1");
       Run2();
       _semaphore.Release();
   }
   static void Run2()
   {
       _semaphore.WaitOne();
       Console.WriteLine("Run2");
       _semaphore.Release();
   }
}

Semafor spowoduje po prostu deadlock, ponieważ aby założyć blokadę w Run2, najpierw trzeba zwolnić ją w Run1, co jest niemożliwe oczywiście. Mutex przechowuje informacje o aktualnym wątku, zatem wie, że w Run2 nie trzeba już zakładać ponownie blokady:

class Program
{
   static Mutex _mutex=new Mutex();

   static void Main(string[] args)
   {
       var t1=Task.Factory.StartNew(Run1);
       Task.WaitAll(t1);            
   }
   static void Run1()
   {
       _mutex.WaitOne();
       Console.WriteLine("Run1");
       Run2();
       _mutex.ReleaseMutex();
   }
   static void Run2()
   {
       _mutex.WaitOne();
       Console.WriteLine("Run2");
       _mutex.ReleaseMutex();
   }
}

Mutex przechowuje tzw. recursion count – jeśli inna metoda wchodzi do sekcji krytycznej, z tego samego wątku, po prostu licznik recursion count jest zwiększany i nie powoduje to blokady. Niestety powyższe korzyści wiążą się z pewnym obciążeniem ponieważ należy aktualizować kilka innych pól. Z tego wynika fakt, że jeśli rekurencja nie jest potrzebna wtedy lepiej unikać klasy Mutex.

Leave a Reply

Your email address will not be published.