ConcurrentDictionary

W dzisiejszym poście kolejna thread-safe kolekcja – słownik danych. Jeśli musimy modyfikować słownik z kilku wątków naraz wtedy ConcurrentDictionary stanowi doskonały wybór. W przypadku gdy chcemy raz uzupełnić słownik a potem tylko czytać z niego dane, wtedy oczywiście nie ma potrzeby wykorzystywania ConcurrentDictionary. Zacznijmy od spisu najważniejszych metod:

  1. TryAdd – dodawanie nowego elementu.
  2. TryUpdate – aktualizacja wpisu.
  3. TryRemove – usuwanie klucza i wartości.
  4. AddOrUpdate – ciekawy twór. Za jednym razem sprawdza czy klucz istnieje i w zależności od tego aktualizuje wartość lub dodaje nowy klucz wraz z wartością.
  5. GetOrAdd – jeśli element istnieje zwraca go, w przeciwnym razie najpierw tworzy wpis.
  6. Count , ToArray, GetEnumerator  – o tym pisałem już przy okazji omawiania poprzednich struktur danych.  Należy zaznaczyć, że w przypadku słownika GetEnumerator nie tworzy snapshot’a i tym samym możemy spodziewać się dirty reads.

TryAdd próbuje dodać nowy wpis. Jeśli klucz już istnieje, metoda zwróci po prostu false.

var dict = new ConcurrentDictionary<string,int>();
if(dict.TryAdd("key", 1))
{
    // dodano element
}
else
{
   // nie dodano elementu poniewaz juz takowy klucz istnieje
}

Analogicznie wygląda sprawa z TryRemove:

ar dict = new ConcurrentDictionary<string,int>();
int removedValue;
if(dict.TryRemove("key",out removedValue))
{
 // usunieto element
}
else
{
 // nie usunieto wartosci poniewaz takowy klucz nie istnieje
}

Warto zwrócić uwagę, że TryRemove również zwraca usuniętą wartość.

TryUpdate jest trochę bardziej skomplikowany. Oprócz podania klucza oraz nowej wartości musimy wskazać jakiej wartości spodziewamy się w słowniku:

var dict = new ConcurrentDictionary<string,int>();
int newValue = 5;
int expectedPreviusValue = 1;
if(dict.TryUpdate("key",newValue,expectedPreviusValue))
{               
}
else
{

}

Wpis zostanie zaktualizowany wyłącznie w sytuacji w której takowy klucz istnieje oraz wartość w nim zapisana jest równa expectedPreviusValue.

Kolejne metody to połączenia Add z Update itp. AddOrUpdate dodaje nowy element lub aktualizuje aktualny jeśli dany klucz już istnieje. Przykład:

var dict = new ConcurrentDictionary<string, int>();
int newValue=dict.AddOrUpdate(key: "key", addValue: 5, updateValueFactory: (key, value) => value + 1);

Pierwszy parametr to klucz, drugi to wartość jaką chcemy dodać. Ostatni to delegate, który jest wywołany w momencie gdy wpis już istnieje. Reasumując powyższe wywołanie sprawdzi czy “key” istnieje w słowniku. Jeśli tak, zostanie wywołana delegata, która zwiększy wartość o jeden. Jeśli takowego wpisu (“key”) nie ma, wtedy jest on dodawany z wartością 5.  Funkcja zwraca wartość jaka została użyta (w naszym przypadku 5 albo ta z delegaty).

GetOrAdd zwraca element a jeśli on nie istnieje to najpierw go dodaje:

var dict = new ConcurrentDictionary<string, int>();
int value = dict.GetOrAdd("key", 5);

Drugie przeładowanie pozwala przekazać wskaźnik na jakąś metodę, która zostanie wykonana w przypadku gdy wpis nie istnieje. Pozwala to w sposób thread-safe wygenerować wartość wyłącznie wtedy gdy nie ma jej w słowniku:

var dict = new ConcurrentDictionary<string, int>();
int value = dict.GetOrAdd("key", (key) => GetValue());

Powyższe operacje są bezpieczne jeśli chodzi o zakleszczenia, zagłodzenie, livelock oraz sekcję krytyczną. Ze względów na optymalizacje nie są one atomowe. W praktyce oznacza to, że metoda, która pierwsza została wykonana wcale nie musi się jako pierwsza zakończyć. Operacje wewnątrz np. AddOrUpdate nie są atomowe. GetOadd wywołuje m.in. delegate w przypadku gdy wpis nie istnieje i należy go wygenerować. W przypadku gdy 2 wątki wywołują GetOrAdd może się zdarzyć, że wątek A wywoła delegate ponieważ wpis nie istnieje a wątek B w tym czasie już zrobi to samo i zakończy działanie przed wątkiem A. Wtedy wątek A,  mimo, że wywołał delegate, zwróci wartość wygenerowaną przez wątek B.

Po mimo kilku dziwnych zachować, kolekcja jest bardzo praktyczna. Szczególnie fajne są metody typu AddorUpdate. Używając zwykłego słownika zawsze trzeba najpierw sprawdzać klucz a tutaj API dostarcza dwie najczęściej używane operacje na raz.

One thought on “ConcurrentDictionary”

  1. Czy orientujesz się dlaczego niektóre kolekcje tworzą snapshot przy enumeracji, a niektóre nie? Jaka stoi za tym racjonalizacja? Pozdrawiam

Leave a Reply

Your email address will not be published.