Artykuł

wrz 02 2012
0

Wielowątkowość w C# - synchronizacja wątków cz. 2 (Semafor, AutoResetEvent i ManualResetEvent, Interlocked)

Tydzień temu w pierwszym poście poświęconym synchronizacji, przedstawiłem Wam trzy kluczowe zagadnienia związane z tym tematem. I choć lock, Monitor oraz Mutex pozwalają nam rozwiązać naprawdę dużą liczbą problemów dotyczących wielowątkowości, to warto wiedzieć, że jest to tylko część programistycznego orężu w jaki wyposażył nas Microsoft. Dziś chciałbym Wam przedstawić cztery kolejne klasy, które powinny uzupełnić temat rozpoczęty tydzień temu i zasadniczo wyczerpać kwestię synchronizacji. Bez zbędnego przedłużania zapraszam zatem do tekstu:)

Semafor

Semafor, czy raczej właściwie Semaphore, to klasa zbliżona i bardzo podobna do Mutexa. Ich podobieństwo związane jest ze wspólnym ojcem ponieważ obie klasy dziedziczą po WaitHandle. W obu przypadkach możemy zatem korzystać z metod WaitOne. Zasadniczą różnicą jest w tym przypadku to, że Semafor w przeciwieństwie do Mutexa umożliwia korzystanie ze strefy krytycznej dowolnej liczbie wątków, którą to my sami definiujemy w konstruktorze.

Najprostszy z konstruktorów, który wystarczy w większości przypadków przyjmuje dwa argumenty. Pierwszy z nich określa nie jako liczbę zajętych miejsc w semaforze. Możemy w tym przypadku podać wartość 0 lub większą (0 oznacza, że wszystkie miejsca są zajęte przez wątek główny), drugi natomiast określa maksymalną liczbę dostępnych miejsc.

Klasa Semafora posiada również odpowiednie metody. Nasze omówienie zaczniemy od WaitOne. Metoda ta zasadniczo wprowadza nas do sekcji krytycznej. Jeśli liczba dostępnych miejsc w danym momencie jest większa niż 0, nasz wątek po prostu przejdzie przez te metodę bez żadnego oczekiwania i rozpocznie realizację zadań zawartych w sekcji krytycznej. W przeciwnym przypadku, będzie musiał zaczekać aż zwolni się jakieś miejsce.

Do zwalniania dostępnych miejsc w Semaforze służy metoda Release, która w wersji bezparametrowej pozwala na zwolnienie jednego miejsca. Istnieje również wersja z parametrem, która pozwala na zwolnienie jednego i więcej miejsc w semaforze w jednej lokalizacji w kodzie. Takie rozwiązanie może być przydatne, w sytuacji gdy kilka wątków kończy pracę w sekcji krytycznej w jednym miejscu.

Spójrzmy na poniższy kod, celem zobrazowania powyższych informacji w praktyce:

class Semafor
{
    private Semaphore m_oSemaphore = null;

    public Semafor()
    {
        m_oSemaphore = new Semaphore(1, 1);
    }

    public void Run()
    {
        try
        {
            m_oSemaphore.WaitOne();
            /// kod sekcji krytycznej
        }
        finally
        {
            m_oSemaphore.Release();
        }
    }
}

Powyższy kod ukazuje to, o czym mówiłem przy okazji Mutexa. Również w tym przypadku musimy się upewnić, że mechanizm zostanie zwolniony, mimo że w przypadku Semafora w niektórych przypadkach nasz kod wciąż może działać, to jednak my powinniśmy mieć pełną kontrolę nad jego działaniem (taka sytuacja może mieć miejsce gdy pozwoliliśmy na bardzo dużą liczbę jednoczesnych wątków), a nie liczyć na przypadek.

ManualResetEvent i AutoResetEvent

Na klasy ManualResetEvent i AutoResetEvent postanowiłem poświęcić jeden akapit, ponieważ są one bliźniaczo podobne do siebie i zasadniczo różni je jeden szczegół, ale o tym trochę szerzej za chwilę.

Idea działania tych klas, polega na blokowaniu dostępu do określonych sekcji, dopóki nie nastąpi sygnalizacja obiektu.

Dostępu do sekcji krytycznej broni oczywiście metoda WaitOne, na której przetwarzanie zostanie wstrzymane, dopóki nie otrzymamy sygnału (metoda Set) na obiekcie typu ResetEvent. Dochodzimy więc powoli teraz do zasadniczej różnicy pomiędzy obiema klasami.

W przypadku klasy AutoResetEvent, sygnalizacja jest automatycznie wyłączana jak tylko wejdziemy do sekcji krytycznej (to jest przekroczymy metodę WaitOne).

W przypadku ManualResetEvent, dzieje się tak tylko w momencie gdy na obiekcie zastosujemy metodę Reset. W przeciwnym razie obiekt cały czas będzie zasygnalizowany i dowolna liczba wątków będzie w stanie wejść do sekcji krytycznej. Zastosowanie ręcznego mechanizmu daje nam większą kontrolę nad całym procesem.

Zasadnicza implementacja obu klas mogłaby wyglądać następująco:

// False, obiekt nie jest zasygnalizowany, musimy wykonać ręczną sygnalizację
ManualResetEvent oManualResetEvent = new ManualResetEvent(false);
// Sygnalizacja
oManualResetEvent.Set();
try
{
    // Oczekiwanie, ponieważ obiekt jest zasygnalizowany od razu wejdziemy
    // do sekcji krytycznej
    oManualResetEvent.WaitOne();
    // Sekcja krytyczna
}
finally
{
    // Blokowanie
    oManualResetEvent.Reset();
}

// False, obiekt nie jest zasygnalizowany, musimy wykonać ręczną sygnalizację
AutoResetEvent oAutoResetEvent = new AutoResetEvent(false);
// Sygnalizacja
oAutoResetEvent.Set();
// Oczekiwanie i zarazem blokowanie. Wejście do sekcji krytycznej 
// nastąpi natychmiastowo
oManualResetEvent.WaitOne();

Oczywiście w rzeczywistym rozwiązaniu, zasadniczy kod byłby rozlokowany w różnych lokalizacjach, jednak już w tym przykładzie warto zwrócić uwagę na kilka detali. W obu przypadkach w konstruktorze definiujemy początkowy status sygnalizacji. Wartość false wymusza zastosowanie metody Set do pierwszego wejścia do sekcji krytycznej. Tak też czynimy w obu przypadkach (4,21).

Następnie standardowo skorzystamy z metody WaitOne (9 i 24). W przypadku braku sygnalizacji przetwarzanie zatrzymałoby się w tym miejscu. Ponieważ jednak na potrzeby powyższego listingu wykonaliśmy metodę Set, wejście do sekcji krytycznej nastąpi natychmiastowo.

W przypadku klasy AutoResetEvent jest to koniec przykładu. Obiekt automatycznie zablokuje dostęp do sekcji krytycznej dla następnej iteracji i nie jest potrzebne żadne dodatkowe działanie, czy też zabezpieczenie w postaci sekcji try..finally.

Inaczej sprawy się mają jeśli chodzi o klasę ManualResetEvent. W tym przypadku sprawy zostały oddane w nasze ręce. Musimy sami zadbać o zablokowanie sekcji krytycznej w dogodnym dla nas momencie. Jest to spora odpowiedzialność, dlatego w tym przypadku powinniśmy pamiętać o sekcji try..finally, która zadba oto by rzeczona wcześniej blokada na pewno się pojawiła. Różnicę między oboma obiektami, jest bardzo łatwo zobrazować w inny sposób. Spójrzcie na poniższy listing:

ManualResetEvent oManualResetEvent = new ManualResetEvent(false);
oManualResetEvent.Set();
oManualResetEvent.WaitOne();
oManualResetEvent.WaitOne();
Console.WriteLine("ManualResetEvent teraz zadziała...");
            
AutoResetEvent oAutoResetEvent = new AutoResetEvent(false);
oAutoResetEvent.Set();
oAutoResetEvent.WaitOne();
oAutoResetEvent.WaitOne();
Console.WriteLine("Ale AutoResetEvent już nie...");

W pierwszym przypadku, przetwarzanie nie zostało w żaden sposób zablokowane. Możemy nawet 100 razy wywołać metodę WaitOne, a bez wywołania metody Reset kod i tak się przetworzy.

W przypadku AutoResetEvent przetwarzanie zatrzyma się na linii 10. Obiekt będzie oczekiwać na ponowną sygnalizację, której w tym przypadku się jednak nie doczeka... Uważajcie na to.

Interlocked

Interlocked to klasa, która różni się od wszystkich przedstawionych powyżej. W istocie nie pozwala ona na tworzenie własnych obiektów, ale jest kontenerem dla szeregu statycznych metod, które umożliwiają przeprowadzanie atomowych operacji na zmiennych, z których mogą korzystać różne wątki. Innymi słowy, dzięki Interlocked możemy bezpiecznie operować na zmiennych oraz ich wartościach. Spójrzmy na kilka przykładowych metod:

  • Add - umożliwia zwiększenie/zmniejszenie wartości liczby całkowitej. Zmienną przekazujemy przez referencję:
    int nTest = 99;
    Interlocked.Add(ref nTest, 1);
  • Decrement - dekrementacja liczby całkowitej:
    int nTest = 99;
    Interlocked.Decrement(ref nTest);
  • Equals - porównanie dowolnych dwóch obiektów:
    string sA = "a";
    string sB = "b";
    Interlocked.Equals(sA, sB);
  • Increment - inkrementacja liczby całkowitej:
    int nTest = 99;
    Interlocked.Increment(ref nTest);
  • Więcej metod znajdziecie w dokumentacji MSDN (link w sekcji literatura).

    Podsumowanie

    Dziś zajęliśmy się kolejnymi czterema klasami, które z pewnością powinny Wam pomóc przy realizacji zadań związanych z synchronizacją. Szczególnie godne uwagi wydają się być klasy z rodziny ...ResetEvent, które można wykorzystać choćby w przytaczanym przeze mnie kilkukrotnie problemie Producenta-Konsumenta (łatwo zasygnalizować wątek konsumenta, przy dodaniu elementu do kolejki właśnie za pomocą tego typu obiektów).

    Dzisiejszy wpis nie jest ostatnim z cyklu. W najbliższym czasie postaram się Wam przybliżyć klasę Timer:)

    Literatura

    Data ostatniej modyfikacji: 06.07.2015, 22:09.

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus