Wiele tekstów mówi, że przy implementacji podwójnie zaznaczonego blokowania w .NET pole, na którym blokujesz, powinno mieć zastosowany modyfikator volatile. Ale dlaczego dokładnie? Biorąc pod uwagę następujący przykład:
public sealed class Singleton
{
private static volatile Singleton instance;
private static object syncRoot = new Object();
private Singleton() {}
public static Singleton Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
}
dlaczego "lock (syncRoot)" nie zapewnia niezbędnej spójności pamięci? Czy nie jest prawdą, że po wyrażeniu „lock” zarówno odczyt, jak i zapis byłyby niestabilne, a więc wymagana spójność została osiągnięta?
Odpowiedzi:
Niestabilność jest niepotrzebna. Cóż, jakby **
volatile
służy do tworzenia bariery pamięci * między odczytami i zapisami w zmiennej.lock
, gdy jest używany, powoduje tworzenie się barier pamięciowych wokół bloku wewnątrzlock
, oprócz ograniczenia dostępu do bloku do jednego wątku.Bariery pamięci sprawiają, że każdy wątek odczytuje najbardziej aktualną wartość zmiennej (a nie wartość lokalną przechowywaną w jakimś rejestrze) i że kompilator nie zmienia kolejności instrukcji. Używanie
volatile
jest niepotrzebne **, ponieważ masz już blokadę.Joseph Albahari wyjaśnia to lepiej niż ja kiedykolwiek potrafiłem.
I koniecznie sprawdź przewodnik Jona Skeeta na temat implementacji singletona w C #
update :
*
volatile
powoduje, że odczyty zmiennej sąVolatileRead
s, a zapisyVolatileWrite
s, które na x86 i x64 w CLR są implementowane z rozszerzeniemMemoryBarrier
. W innych systemach mogą być drobniejsze.** moja odpowiedź jest poprawna tylko wtedy, gdy używasz CLR na procesorach x86 i x64. Może to być prawdą w innych modelach pamięci, takich jak Mono (i inne implementacje), Itanium64 i przyszły sprzęt. To jest to, do czego odnosi się Jon w swoim artykule w "pułapkach" podwójnie sprawdzanego blokowania.
Wykonanie jednego z {oznaczanie zmiennej jako
volatile
, odczytywanie jejThread.VolatileRead
lub wstawianie wywołaniaThread.MemoryBarrier
} może być konieczne, aby kod działał poprawnie w sytuacji ze słabym modelem pamięci.Z tego, co rozumiem, w CLR (nawet na IA64) zapisy nigdy nie są zmieniane (zapisy zawsze mają semantykę wydania). Jednak na IA64 odczyty mogą być zmieniane w kolejności przed zapisami, chyba że są oznaczone jako lotne. Niestety nie mam dostępu do sprzętu IA64 do zabawy, więc cokolwiek na ten temat powiem, byłoby spekulacją.
Przydały mi się również te artykuły:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
artykuł vance'a morrisona (wszystko się do tego odwołuje, mówi o podwójnie sprawdzanym blokowaniu)
artykuł chrisa brumme'a (wszystkie linki do tego )
Joe Duffy: Zepsute warianty podwójnie sprawdzanego blokowania
Seria luis abreu na temat wielowątkowości również daje ładny przegląd pojęć
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http: // msmvps. pl / blogs / luisabreu / archive / 2009/07/03 / multithreading-introducing-memory-fences.aspx
źródło
volatile
była niepotrzebna na dowolnej platformie wtedy oznaczałoby to JIT nie mógł zoptymalizować obciążenia pamięciobject s1 = syncRoot; object s2 = syncRoot;
, abyobject s1 = syncRoot; object s2 = s1;
na tej platformie. Wydaje mi się to mało prawdopodobne.Jest sposób na wdrożenie go bez
volatile
pola. Wyjaśnię to ...Myślę, że to zmiana kolejności dostępu do pamięci wewnątrz zamka jest niebezpieczna, tak że można uzyskać niecałkowicie zainicjowaną instancję poza zamkiem. Aby tego uniknąć, robię to:
public sealed class Singleton { private static Singleton instance; private static object syncRoot = new Object(); private Singleton() {} public static Singleton Instance { get { // very fast test, without implicit memory barriers or locks if (instance == null) { lock (syncRoot) { if (instance == null) { var temp = new Singleton(); // ensures that the instance is well initialized, // and only then, it assigns the static variable. System.Threading.Thread.MemoryBarrier(); instance = temp; } } } return instance; } } }
Zrozumienie kodu
Wyobraź sobie, że wewnątrz konstruktora klasy Singleton znajduje się kod inicjujący. Jeśli kolejność tych instrukcji zostanie zmieniona po ustawieniu pola na adres nowego obiektu, oznacza to, że masz niekompletną instancję ... wyobraź sobie, że klasa ma taki kod:
private int _value; public int Value { get { return this._value; } } private Singleton() { this._value = 1; }
Teraz wyobraź sobie wywołanie konstruktora za pomocą nowego operatora:
instance = new Singleton();
Można to rozszerzyć na następujące operacje:
ptr = allocate memory for Singleton; set ptr._value to 1; set Singleton.instance to ptr;
Co się stanie, jeśli zmienię kolejność tych instrukcji w ten sposób:
ptr = allocate memory for Singleton; set Singleton.instance to ptr; set ptr._value to 1;
Czy to robi różnicę? NIE, jeśli myślisz o pojedynczym wątku. TAK, jeśli myślisz o wielu wątkach ... co jeśli wątek zostanie przerwany tuż po
set instance to ptr
:ptr = allocate memory for Singleton; set Singleton.instance to ptr; -- thread interruped here, this can happen inside a lock -- set ptr._value to 1; -- Singleton.instance is not completelly initialized
Tego właśnie unika bariera pamięci, nie zezwalając na zmianę kolejności dostępu do pamięci:
ptr = allocate memory for Singleton; set temp to ptr; // temp is a local variable (that is important) set ptr._value to 1; -- memory barrier... cannot reorder writes after this point, or reads before it -- -- Singleton.instance is still null -- set Singleton.instance to temp;
Miłego kodowania!
źródło
new
jest w pełni zainicjowany ..Myślę, że nikt nie odpowiedział na to pytanie , więc spróbuję.
Zmienne i pierwsze
if (instance == null)
nie są „konieczne”. Blokada sprawi, że ten kod będzie bezpieczny dla wątków.Więc pytanie brzmi: dlaczego miałbyś dodać pierwszy
if (instance == null)
?Powodem jest przypuszczalnie unikanie niepotrzebnego wykonywania zablokowanej sekcji kodu. Podczas wykonywania kodu wewnątrz zamka każdy inny wątek, który próbuje również wykonać ten kod, jest blokowany, co spowolni twój program, jeśli będziesz często próbował uzyskać dostęp do singletona z wielu wątków. W zależności od języka / platformy mogą również występować narzuty związane z samym zamkiem, których chcesz uniknąć.
Tak więc pierwsze sprawdzenie zerowe jest dodawane jako naprawdę szybki sposób sprawdzenia, czy potrzebujesz blokady. Jeśli nie musisz tworzyć singletona, możesz całkowicie uniknąć blokady.
Ale nie możesz sprawdzić, czy odwołanie jest zerowe bez blokowania go w jakiś sposób, ponieważ z powodu buforowania procesora inny wątek mógłby go zmienić i odczytać „nieaktualną” wartość, która prowadziłaby do niepotrzebnego wejścia do blokady. Ale próbujesz uniknąć blokady!
Więc sprawiasz, że singleton jest zmienny, aby mieć pewność, że czytasz najnowszą wartość, bez konieczności używania blokady.
Nadal potrzebujesz wewnętrznego zamka, ponieważ niestabilność chroni Cię tylko podczas pojedynczego dostępu do zmiennej - nie możesz go bezpiecznie przetestować i ustawić bez użycia zamka.
Czy to rzeczywiście przydatne?
Cóż, powiedziałbym „w większości przypadków nie”.
Jeśli Singleton.Instance może powodować nieefektywność z powodu blokad, to dlaczego dzwonisz do niego tak często, że byłby to znaczący problem ? Cały sens singletona polega na tym, że jest tylko jeden, więc kod może raz odczytać i buforować pojedyncze odniesienie.
Jedynym przypadkiem, w którym mogę wymyślić, gdzie to buforowanie nie byłoby możliwe, byłby, gdy masz dużą liczbę wątków (np. Serwer używający nowego wątku do przetwarzania każdego żądania może tworzyć miliony bardzo krótko działających wątków, każdy z co musiałoby raz zadzwonić do Singleton.Instance).
Podejrzewam więc, że podwójnie sprawdzane blokowanie jest mechanizmem, który ma realne miejsce w bardzo specyficznych przypadkach krytycznych dla wydajności, a następnie każdy wdrapał się na modę „to jest właściwy sposób”, nie zastanawiając się, co robi będzie faktycznie konieczne w przypadku, gdy go używają.
źródło
volatile
nie ma nic wspólnego z semantyką blokad w blokowaniu podwójnie sprawdzanym, ma to związek z modelem pamięci i spójnością pamięci podręcznej. Jego celem jest zapewnienie, że jeden wątek nie otrzyma wartości, która jest nadal inicjowana przez inny wątek, czego nie zapobiega wzorzec blokady podwójnego sprawdzenia. W Javie zdecydowanie potrzebujeszvolatile
słowa kluczowego; w .NET jest mętny, ponieważ jest zły według ECMA, ale poprawny według środowiska wykonawczego. Tak czy inaczej,lock
zdecydowanie się tym nie zajmuje.lock
sprawia, że kod jest bezpieczny dla wątków. Ta część jest prawdą, ale wzorzec podwójnego sprawdzania blokady może uczynić ją niebezpieczną . Wydaje się, że tego właśnie brakuje. Ta odpowiedź wydaje się błądzić o znaczeniu i celu podwójnego sprawdzenia blokady bez rozwiązywania problemów związanych z bezpieczeństwem wątków, które są tego przyczynąvolatile
.instance
jest oznaczonyvolatile
?Powinieneś używać volatile ze wzorem podwójnej blokady.
Większość ludzi wskazuje ten artykuł jako dowód, że nie potrzebujesz ulotności: https://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10
Ale nie czytają do końca: „ Ostatnie słowo ostrzeżenia - zgaduję tylko model pamięci x86 na podstawie zaobserwowanego zachowania na istniejących procesorach. Dlatego techniki low-lock są również delikatne, ponieważ sprzęt i kompilatory mogą z czasem stać się bardziej agresywne . Oto kilka strategii minimalizowania wpływu tej kruchości na twój kod. Po pierwsze, jeśli to możliwe, unikaj technik o niskiej blokadzie. (...) Na koniec załóż najsłabszy możliwy model pamięci, używając deklaracji nietrwałych, zamiast polegać na niejawnych gwarancjach ”.
Jeśli potrzebujesz więcej przekonywania, przeczytaj ten artykuł na temat specyfikacji ECMA, która będzie używana na innych platformach: msdn.microsoft.com/en-us/magazine/jj863136.aspx
Jeśli potrzebujesz dalszych przekonań, przeczytaj ten nowszy artykuł, że można wprowadzić optymalizacje, które uniemożliwiają działanie bez ulotności: msdn.microsoft.com/en-us/magazine/jj883956.aspx
Podsumowując, "może" zadziałać dla ciebie bez ulotności w tej chwili, ale nie ryzykuj, że napisze odpowiedni kod i albo użyje metody volatile lub volatileread / write. Artykuły, które sugerują, że należy postąpić inaczej, czasami pomijają niektóre z możliwych zagrożeń związanych z optymalizacją JIT / kompilatora, które mogą mieć wpływ na Twój kod, a także z naszymi przyszłymi optymalizacjami, które mogą się zdarzyć, które mogą spowodować uszkodzenie kodu. Również, jak wspomniano w poprzednim artykule, poprzednie założenia pracy bez zmienności mogą już nie wytrzymać ARM.
źródło
AFAIK (i - uważaj na to, nie robię wielu rzeczy jednocześnie) nie. Blokada po prostu zapewnia synchronizację między wieloma rywalami (wątkami).
niestabilny z drugiej strony mówi twojemu komputerowi, aby za każdym razem ponownie oceniał wartość, abyś nie natknął się na buforowaną (i złą) wartość.
Zobacz http://msdn.microsoft.com/en-us/library/ms998558.aspx i zwróć uwagę na następujący cytat:
Opis volatile: http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx
źródło
Myślę, że znalazłem to, czego szukałem. Szczegóły w tym artykule - http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10 .
Podsumowując - w .NET modyfikator lotny rzeczywiście nie jest potrzebny w tej sytuacji. Jednak w słabszych modelach pamięci zapisy wykonane w konstruktorze leniwie zainicjowanego obiektu mogą być opóźnione po zapisie do pola, więc inne wątki mogą odczytać uszkodzoną instancję niezerową w pierwszej instrukcji if.
źródło
lock
Jest wystarczająca. Sama specyfikacja języka MS (3.0) wspomina dokładnie o tym scenariuszu w §8.12, bez żadnej wzmianki ovolatile
:źródło
To całkiem niezły post o używaniu volatile z podwójnie sprawdzonym blokowaniem:
http://tech.puredanger.com/2007/06/15/double-checked-locking/
W Javie, jeśli celem jest ochrona zmiennej, nie musisz jej blokować, jeśli jest oznaczona jako niestabilna
źródło