Udało mi się zaimplementować słownik bezpieczny dla wątków w C #, wyprowadzając z IDictionary i definiując prywatny obiekt SyncRoot:
public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
private readonly object syncRoot = new object();
private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();
public object SyncRoot
{
get { return syncRoot; }
}
public void Add(TKey key, TValue value)
{
lock (syncRoot)
{
d.Add(key, value);
}
}
// more IDictionary members...
}
Następnie blokuję ten obiekt SyncRoot wśród moich konsumentów (wiele wątków):
Przykład:
lock (m_MySharedDictionary.SyncRoot)
{
m_MySharedDictionary.Add(...);
}
Udało mi się to zrobić, ale zaowocowało to brzydkim kodem. Moje pytanie brzmi: czy istnieje lepszy, bardziej elegancki sposób implementacji słownika bezpiecznego dla wątków?
Odpowiedzi:
Jak powiedział Peter, możesz zawrzeć całość bezpieczeństwa wątków wewnątrz klasy. Będziesz musiał uważać na wszelkie zdarzenia, które ujawnisz lub dodasz, upewniając się, że są wywoływane poza jakimikolwiek zamkami.
Edycja: dokumentacja MSDN wskazuje, że wyliczanie z natury nie jest bezpieczne dla wątków. Może to być jeden z powodów ujawniania obiektu synchronizacji poza klasą. Innym sposobem podejścia byłoby zapewnienie pewnych metod wykonywania akcji na wszystkich członkach i blokowanie wyliczania członków. Problem polega na tym, że nie wiesz, czy akcja przekazana do tej funkcji wywołuje jakiegoś członka słownika (co spowodowałoby zakleszczenie). Ujawnienie obiektu synchronizacji umożliwia konsumentowi podjęcie tych decyzji i nie ukrywa zakleszczenia wewnątrz klasy.
źródło
Klasa .NET 4.0, która obsługuje współbieżność, nosi nazwę
ConcurrentDictionary
.źródło
Próba wewnętrznej synchronizacji prawie na pewno będzie niewystarczająca, ponieważ jej poziom abstrakcji jest zbyt niski. Załóżmy, że operacje
Add
i sąContainsKey
indywidualnie bezpieczne dla wątków w następujący sposób:Co się dzieje, gdy wywołujesz ten rzekomo bezpieczny dla wątków fragment kodu z wielu wątków? Czy zawsze będzie działać dobrze?
Prostą odpowiedzią jest: nie. W pewnym momencie
Add
metoda zgłosi wyjątek wskazujący, że klucz już istnieje w słowniku. Możesz zapytać, jak to może być z bezpiecznym słownikiem? Cóż, tylko dlatego, że każda operacja jest bezpieczna dla wątków, połączenie dwóch operacji nie jest, ponieważ inny wątek mógłby zmodyfikować ją między wywołaniemContainsKey
aAdd
.Co oznacza, że aby poprawnie napisać tego typu scenariusz, potrzebujesz blokady poza słownikiem, np
Ale teraz, widząc, że musisz napisać zewnętrzny kod blokujący, mieszasz synchronizację wewnętrzną i zewnętrzną, co zawsze prowadzi do problemów, takich jak niejasny kod i zakleszczenia. Więc ostatecznie prawdopodobnie lepiej:
Użyj normalnego
Dictionary<TKey, TValue>
i zsynchronizuj zewnętrznie, zamykając na nim operacje złożone lubNapisz nową otokę bezpieczną dla wątków z innym interfejsem (tj. Nie
IDictionary<T>
), która łączy operacje, takie jakAddIfNotContained
metoda, więc nigdy nie musisz łączyć operacji z niej.(Sam mam tendencję do # 1)
źródło
Nie powinieneś publikować swojego prywatnego obiektu blokady za pośrednictwem właściwości. Obiekt blokady powinien istnieć prywatnie wyłącznie w celu pełnienia funkcji punktu spotkania.
Jeśli wydajność okaże się słaba przy użyciu standardowego zamka, kolekcja zamków Power Threading firmy Wintellect może być bardzo przydatna.
źródło
Istnieje kilka problemów związanych z metodą implementacji, którą opisujesz.
Osobiście odkryłem, że najlepszym sposobem implementacji klasy bezpiecznej dla wątków jest niezmienność. To naprawdę zmniejsza liczbę problemów, które możesz napotkać z bezpieczeństwem wątków. Sprawdź Eric Lippert na bloga po więcej szczegółów.
źródło
Nie musisz blokować właściwości SyncRoot w obiektach konsumenckich. Blokada, którą masz w metodach słownika, jest wystarczająca.
Do opracowania: kończy się to, że słownik jest zablokowany na dłuższy okres czasu, niż jest to konieczne.
Oto co dzieje się w Twoim przypadku:
Powiedz, że wątek A uzyskuje blokadę w SyncRoot przed wywołaniem m_mySharedDictionary.Add. Wątek B następnie próbuje uzyskać blokadę, ale jest blokowany. W rzeczywistości wszystkie inne wątki są blokowane. Wątek A może wywoływać metodę Add. W instrukcji lock w metodzie Add wątek A może ponownie uzyskać blokadę, ponieważ już jest jej właścicielem. Po wyjściu z kontekstu blokady w ramach metody, a następnie poza metodą, wątek A zwolnił wszystkie blokady, umożliwiając kontynuację innych wątków.
Możesz po prostu zezwolić dowolnemu konsumentowi na wywołanie metody Add, ponieważ instrukcja lock w metodzie Add klasy SharedDictionary będzie miała ten sam efekt. W tym momencie masz nadmiarowe blokowanie. Zablokowałbyś tylko SyncRoot poza jedną z metod słownika, gdybyś musiał wykonać dwie operacje na obiekcie słownika, który wymagał zagwarantowania, że wystąpią po kolei.
źródło
Pomyśl, dlaczego nie odtworzyć słownika? Jeśli czytanie to wiele zapisów, blokowanie zsynchronizuje wszystkie żądania.
przykład
źródło
Kolekcje i synchronizacja
źródło