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.