Synchronizacja za pomocą CountdownEvent

W ostatnich postach opisuje różne mechanizmy synchronizacji w c#. Jak widać, jest tego na prawdę wiele i sama znajomość słowa lock nie wystarcza jeśli poważnie myśli się o algorytmach współbieżnych. Dziś kolejna metoda synchronizacji a mianowicie klasa CountdownEvent. Ostatnie posty oprócz wprowadzenia do wspomnianych klas stanowią wstęp do kolekcji współbieżnych, które zamierzam omówić wkrótce.

Konstruktor CountdownEvent przyjmuje liczbę całkowita (zwykle większą od 0), która stanowi wartość początkową wewnętrznego licznika. Następnie podczas wywołania metody Wait, wątek jest blokowany dopóki licznik nie osiągnie wartości zero. Licznik jest zmniejszany przez metodę Signal, która ma dwa przeładowania. Jedno zmniejsza licznik po prostu o 1 a drugi o wartość przekazaną w parametrze:

var countDownEvent = new CountdownEvent(20);
countDownEvent.Signal(15); 

Jedna bardzo ważna uwaga. CountdownEvent używa mechanizmu Spin opisanego kilka postów wcześniej. Z tego względu, wykorzystanie klasy jest wydajne jednak wyłącznie gdy wiemy, że “blokada” została nałożona na krótki czas. W innych scenariuszach, może się okazać, że wątek blokuje zasoby i lepiej aby został uśpiony ponieważ będzie musiał poczekać trochę na swoją kolej. Polecam przeczytanie tego posta aby w pełni zrozumieć zasadę działania Spin i czym ona się różni od klasycznych blokad.

Analogiczną metodą jest AddCount, która zwiększa licznik:

var countDownEvent = new CountdownEvent(20);
countDownEvent.AddCount(15); 

Metoda Wait blokuje wątek i czeka aż licznik zostanie ustawiony na zero (np. poprzez wysyłanie sygnału za pomocą Signal):

_countdownEvent.Wait();
// jakas logika tutaj

Ponadto można przekazać timeout w milisekundach:

_countdownEvent.Wait(5000);

Z kolei metoda Reset ustawia CurrentCount do InitialCount:

_countdownEvent.Reset();

Aby przekonać się, że CountDownEvent używa SpinWait, sprawdźmy implementację Signal np. w Reflector:

public bool Signal(int signalCount)
{
    int num;
    if (signalCount <= 0)
    {
        throw new ArgumentOutOfRangeException("signalCount");
    }
    this.ThrowIfDisposed();
    SpinWait wait = new SpinWait();
Label_001D:
    num = this.m_currentCount;
    if (num < signalCount)
    {
        throw new InvalidOperationException(Environment.GetResourceString("CountdownEvent_Decrement_BelowZero"));
    }
    if (Interlocked.CompareExchange(ref this.m_currentCount, num - signalCount, num) != num)
    {
        wait.SpinOnce();
        goto Label_001D;
    }
    if (num == signalCount)
    {
        this.m_event.Set();
        return true;
    }
    return false;
}

Signal korzysta z klasy SpinWait, którą omówię w następnym poście ale mechanizm jest analogiczny do SpinLock opisanego w jednym z poprzednich postów – nie wymaga zmiany kontekstu.

Zasada działania CountdownEvent jest podobna do ManualResetEvent z tym, że mamy do dyspozycji licznik liczb całkowitych a nie zwykły boolean. Przykłady zastosowań są również podobne np. synchronizacja zdarzeń.  Podsumowując najważniejsze metody to Wait, AddCount, Signal oraz Reset. Wszystkie poza Wait zmniejszają lub zwiększają wewnętrzny licznik a  Wait blokuje wątek dopóki licznik nie osiągnie wartości zero.

Leave a Reply

Your email address will not be published.