ReaderWriterLockSlim – synchronizacja danych

ReaderWriterLockSlim jest klasą, która ma zastąpić ReadWriterLock, znanego ze starych wersji framework’a. Ale zacznijmy od początku…

Dlaczego zwykły lock nie zawsze jest wystarczający? ReaderWriterLockSlim pracuje w trzech trybach:

  • mutual lock – inaczej writer lock. Wyłącznie jedna taka blokada może zostać nadana. Jest to typowy lock i należy z niego korzystać, gdy modyfikujemy dane.
  • shared (reader) lock – wiele wątków może mieć dostęp do tych samych danych. Można nadać wiele shared lock, pod warunkiem, że mutual lock nie jest nałożony.
  • upgrade mode – służy do zmiany shared lock w mutual lock.
  • Innymi słowy ReaderWriterLockSlim może nadać dostęp wielu wątkom jednocześnie, pod warunkiem, że nie modyfikują one danych – w końcu jeśli tylko czytają jest to bezpieczne. Jeśli tylko jakiś wątek będzie chciał zmodyfikować dane, wtedy konieczne jest nałożenie mutual lock i wyłączenie wszystkich shared\upgrade.

    Rozważmy przykład z MSDN:

    public class SynchronizedCache
    {
        private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
        private Dictionary<int, string> innerCache = new Dictionary<int, string>();
    
        public string Read(int key)
        {
            cacheLock.EnterReadLock();
            try
            {
                return innerCache[key];
            }
            finally
            {
                cacheLock.ExitReadLock();
            }
        }
        public void Add(int key, string value)
        {
            cacheLock.EnterWriteLock();
            try
            {
                innerCache.Add(key, value);
            }
            finally
            {
                cacheLock.ExitWriteLock();
            }
        }
    }

    Funkcja Read może zostać wywołana jednocześnie z wielu wątków. Z kolei, jeśli ktoś będzie chciał wywołać Add, wtedy Read nie jest możliwe. Proszę zauważyć jak to znacząco może zwiększyć wydajność. Klasycznym rozwiązaniem byłoby umieszczenie Read w lock. Niestety w takiej sytuacji, tylko jeden wątek mógłby czytać dane, jeśli nawet nie są one aktualnie modyfikowane.

    Ktoś może zadać pytanie, po co nam jakieś blokady gdy chcemy czytać tylko dane? Czy odczyt nie jest zawsze bezpieczny? Odczyt jest bezpieczny jeśli akurat dane nie są modyfikowane. Co jeśli np. Add dodał klucz do słownika a nie ustawił jeszcze danych? W końcu nie są to operacje atomowe. W takim przypadku, brak jakiejkolwiek blokady skutkowałby odczytem nieprawidłowych (częściowych) danych.

    Istnieje jeszcze jeden scenariusz. Po odczycie danych, może okazać się, że niezbędna jest ich modyfikacja. W takim przypadku używamy trzeciego trybu – upgrade. Załóżmy, że chcemy zaimplementować funkcje AddOrUpdate. Nie chcemy przy typ blokować całego kodu. Możemy w końcu najpierw odczytać dane i jeśli wartość jest taka sama jak parametr wejściowy wtedy w ogóle nic nie musimy robić. Bardzo to zoptymalizuje wywołania, które nie zmieniają stanu:

    public AddOrUpdateStatus AddOrUpdate(int key, string value)
    {
       cacheLock.EnterUpgradeableReadLock();
       try
       {
           string result = null;
           if (innerCache.TryGetValue(key, out result))
           {
               if (result == value)
               {
                   return AddOrUpdateStatus.Unchanged;
               }
               else
               {
                   cacheLock.EnterWriteLock();
                   try
                   {
                       innerCache[key] = value;
                   }
                   finally
                   {
                       cacheLock.ExitWriteLock();
                   }
                   return AddOrUpdateStatus.Updated;
               }
           }
           else
           {
               cacheLock.EnterWriteLock();
               try
               {
                   innerCache.Add(key, value);
               }
               finally
               {
                   cacheLock.ExitWriteLock();
               }
               return AddOrUpdateStatus.Added;
           }
       }
       finally
       {
           cacheLock.ExitUpgradeableReadLock();
       }
    }

    EnterUpgradeableReadLock wchodzi w tryb Upgradeable. W przypadku takiej blokady, żaden z mutual lock nie może być nałożony. Dozwolone jednak są reader’y, które aktualnie są już nałożone – nowe nie mogą mieć już miejsca. Następnie po przeczytaniu stanu można zdecydować, czy chcemy uzyskać dostęp wyłączony czy nie. Jeśli tak, po prostu wywołujemy EnterWriteLock. Proszę zauważyć, że upgradeable lock nie służy do modyfikacji danych! Wyłącznie bezpieczne w nim jest przeczytanie stanu i zdecydowanie następnie czy chcemy przełączyć się z powrotem do read czy jednak do writer. Wynika z tego, że powinniśmy jak najszybciej podjąć taką decyzje – upgradeable tym różni się od read, że nie dopuszcza nowych reader’ow.

    Podejrzewam, ze tryb upgradeable wprowadził trochę zamieszania. W następnym wpisie wyjaśnię różnice między starym ReadWriterLock a nowym  i przy okazji stanie się jasne, dlaczego ten tryb jest konieczny.

    Leave a Reply

    Your email address will not be published.