Sprawdzanie warunku w środowisku wielowątkowym

Dzisiaj zajmiemy się kolejnym wzorcem przeznaczonym dla środowiska wielowątkowego. W dokumentacji\artykułach możemy go spotkać pod nazwą “condition pattern”. Załóżmy, że jeden wątek musi sprawdzić pewien warunek aby móc wykonać jakąś pracę. Innymi słowy, mamy współdzielony zestaw zmiennych, modyfikowanych przez różne wątki. Jeden z wątków może wykonać swój kod wyłącznie, gdy te współdzielone zmienne spełnią jakiś warunek.

W jaki sposób moglibyśmy podejść do problemu? Najprostszym rozwiązaniem byłaby pętla while, blokująca (lock) co jakiś dostęp do danych i sprawdzająca czy warunek został już spełniony. Niestety spowoduje to bardzo dużo zmian kontekstu – każdy lock to ogromny nakład pracy dla CPU. Ponadto, wątek byłby budzony bardzo często co nie ma przecież sensu.

W systemie Windows istnieją tzw. condition variable. Bezpośrednio w .NET nie mamy do nich dostępu ale jak ktoś chce sobie poczytać to zapraszam tutaj.

.NET wspiera condition variable w mniej bezpośredni sposób ale za to w dużo łatwiejszy. Możemy użyć do tego klasy Monitor i metod Wait\Pulse\PulseAll. Wszystkie one bazują na condition variable.

Dla przypomnienia dodam, że klasa Monitor to implementacja słowa lock. Pisząc lock{} to nic innego jak wywołanie Monitor.Enter i Monitor.Exit. Z tego względu, wszyscy z nas korzystali z niej wielokrotnie, głównie w sposób pośredni przez lock. Metody Wait\Pulse są jednak dużo mniej znane.

Wywołanie Monitor.Wait powoduje, że wątek jest dołączany do specjalnej kolekcji czekających wątków. Powoduje to uśpienie wątku, zmianę kontekstu itp. Z kolei, jeśli chcemy obudzić wątek, wtedy wywołujemy Pulse.

Rozważmy to na przykładzie:

class ConditionPatternSample
{
   private readonly Object _lock = new Object();
   private Boolean _condition = false;

   public void Thread1()
   {
       Monitor.Enter(_lock);

       while (!_condition)
       {
           Monitor.Wait(_lock);
       }

       Monitor.Exit(_lock);
   }

   public void Thread2()
   {
       Monitor.Enter(_lock);

       _condition = true;

       Monitor.Pulse(_lock);

       Monitor.Exit(_lock);
   }
}

Thread1 to wątek sprawdzający warunek z kolei Thread2 modyfikuje go. W obu przypadkach chcemy mieć dostęp wyłączony stąd potrzebujemy synchronizacji. W momencie gdy condition jest równy false, wtedy Monitor.Wait tymczasowo zwalnia blokadę tak, że Thread2 może wejść do swojej sekcji krytycznej. Następnie, po ustawieniu _condition na true, drugi wątek wysyła sygnał za pomocą Monitor.Pulse, który obudzi dokładnie jeden wątek, który wcześniej wykonał Monitor.Wait. Oczywiście Thread1 będzie mógł zostać wybudzony wyłącznie w momencie gdy, Thread2 wywoła również Monitor.Exit.

Istnieje jeszcze druga analogiczna metoda, Monitor.PulseAll. W przeciwieństwie do Pulse, wysyła ona sygnał do wszystkich czekających wątków a nie tylko do pierwszego w kolekcje. Nie oznacza to, że wszystkie one zostaną jednocześnie wybudzone – to jest sekcja krytyczna więc jednocześnie tylko jeden ma prawo wykonywać swój kod. Podsumowując:

  1. Zarówno Wait jak i Pulse\PulseAll muszą być w sekcji krytycznej (Enter – Exit).
  2. Wait umieszcza wątek w kolejce  typu FIFO wątków oczekujących.
  3. Pulse budzi pierwszy wątek z kolejki.
  4. PulseAll budzi wszystkie wątki z kolejki aczkolwiek wszystkie one zostaną uszeregowane w sposób sekwencyjny aby nie łamać sekcji krytycznej.

Przyjrzyjmy się teraz bliżej jeszcze raz Monitor.Wait:

while (!_condition)
{
 Monitor.Wait(_lock);
}

Dlaczego zawsze należy umieszczać Wait w while? W końcu moglibyśmy napisać coś takiego:

if (!_condition)
{
 Monitor.Wait(_lock);
}

Jeśli warunek jest nieprawdziwy wtedy uśpij wątek. W praktyce jednak istnieje ryzyko, że wątek zostanie wybudzony pomimo, że tak naprawdę Pulse nie został wywołany – a co za tym idzie, warunek również nie został zmieniony. Z tego względu, nie możemy polegać na tym, że wątek wybudzony to na 100% znaczy, że Pulse został wywołany. Inna przyczyna (dużo częstsza w implementacji .NET dla Windows) to fakt, że pomiędzy wywołaniem Pulse, a wybudzeniem danego wątku może okazać się, że jeszcze inny wątek wkradł się i ustawił condition z powrotem na false.