W dzisiejszym poście przedstawię zasadę działania semafora oraz mutexa. Zacznijmy od teorii, czym jest semafor i jak można go zaimplementować? Otóż semafor jest sposobem na realizację wzajemnego wykluczania – zapewnienia, że tylko określona liczba wątków będzie mogła jednocześnie wykonać dany fragment kodu. Wyróżniamy semafory binarne, które dopuszczają maksymalnie jeden wątek oraz semafory ogólne, które umożliwiają jednoczesny dostęp określoną przez programistę liczbę wątków.
Implementacja semafora wymaga zaprogramowania dwóch metod: wait oraz signal. Programista wchodząc do sekcji krytycznej wywołuję wait a wychodząc wykonuje signal. Zatem kod źródłowy semafora ogólnego mógłby wyglądać następująco:
// ustawienie wartośći początkowej semafora counter=5; // metody atomowe void Wait() { while(counter<=0){} counter--; } void Signal() { counter++; }
Jeśli zasoby aktualnie są używane, metoda Wait będzie blokować dalsze wykonywanie kodu. Dopiero w momencie gdy sekcja krytyczna zostanie opuszczona(wywołanie Signal), warunek w Wait zostanie spełniony i dalszy kod będzie mógł się wykonać. Należy zaznaczyć, że metody muszą być atomowe ponieważ w przeciwnym razie mogą wystąpić problemy synchronizacyjne opisane w poprzednim poście. W przypadku gdy counter posiada wartość początkową równą 1, mamy do czynienia z semaforem binarnym.
Powyższy kod pokazałem tylko po to aby rozjaśnić sposób działania semafora. C# posiada bowiem gotową klasę System.Threading.Semaphore:
System.Threading.Semaphore semaphore = new System.Threading.Semaphore(1, 1); semaphore.WaitOne(); // sekcja krytyczna semaphore.Release(1);
Konstruktor przyjmuje kolejno aktualną oraz maksymalną wartość licznika counter.
Przejdźmy teraz do następnego mechanizmu synchronizacji – Mutex’a. Mutex od strony użytkownika wygląda bardzo podobnie do semafora. Umożliwia jednak synchronizację na poziomie procesów a nie tylko wewnątrz AppDomain.
Należy również wspomnieć o zasadzie “principle of ownership” – tylko wątek który nałożył blokadę może ją później zdjąć. W przeciwieństwie do semaforów, nie możemy ustawić blokady w wątku A a zdjąć jej w wątku B – zakończy się to wyrzuceniem wyjątku.
Klasycznym przykładem wykorzystania mutexów jest zapewnienie, że tylko jedna instancja programu zostanie uruchomiona:
class Program { static void Main(string[] args) { Mutex oneMutex = null; const string MutexName = "SingleInstance"; try { oneMutex = Mutex.OpenExisting(MutexName); } catch (WaitHandleCannotBeOpenedException) { // Mutex nie istnieje, obsługa wyjątku } if (oneMutex == null) { oneMutex = new Mutex(true, MutexName); } else { oneMutex.Close(); return; } // tworzenie okna itp. } }
Z przykładu widać, że podczas tworzenia obiektu Mutex można przekazać jego nazwę, która służyć będzie do rozpoznawania obiektu w różnych procesach. W przypadku gdy funkcja OpenExisting zwróci wartość różną od NULL(Mutex już utworzony), program zakończy działanie ponieważ zostanie wykonana instrukcja “return;”.
Oczywiście Mutex posiada również metody WaitOne oraz ReleaseMutex. Sposób użytkowania jest niemalże identyczny jak w przypadku semafora binarnego, więc nie będę pokazywał już kodu źródłowego.
W następnym poście planuje opisać klasy ManualResetEvent, AutoResetEvent oraz Interlocked – zapraszam do odwiedzenia bloga za kilka dni.