Załóżmy, że istnieje klasa zawierająca publiczne pole licznika int, do którego może mieć dostęp wiele wątków, a ta liczba będzie tylko rosła lub malała.
Dodając to pole, które z poniższych schematów należy użyć i dlaczego?
- lock(this.locker) this.counter++;
- Interlocked.Increment (ref this.counter);
- Zmień modyfikator dostępu z counter na public volatile
Najgorsze (żadne z nich tak naprawdę nie działa)
Zmień modyfikator dostępu z counter na public volatile
To podejście wcale nie jest bezpieczne, a istotą Volatile jest to, że wiele wątków działających na wielu procesorach buforuje dane i przestawia wykonywane instrukcje.
Jeśli jest nieulotny, gdy CPU A wzrasta o jakąś wartość, CPU B musi chwilę poczekać, aby zobaczyć wzrost wartości, co może prowadzić do problemów.
Jeśli jest to ulotność, zapewnia, że oba procesory widzą tę samą wartość w tym samym czasie. Ale nie unika to przekrojowych operacji odczytu i zapisu.
Dodanie wartości do zmiennej faktycznie wymaga trzech kroków
1. Czytanie, 2. Dodaj 3. Napisz
Załóżmy, że wątek A odczytuje wartość licznika jako 1 i nie jest gotowy do wzrostu, to wątek B również odczytuje wartość licznika jako 1, a oba wątki zaczynają wykonywać operacje przyrostowe i zapisujące. Wartość ostatniego licznika to 2. To nie jest poprawne, oba wątki wykonały operację wzrostu, a poprawny wynik powinien wynosić 3. Więc oznaczanie go jako niestabilnego jest po prostu niebezpieczne.
Lepiej
lock(this.locker) this.counter++;
W ten sposób jest bezpiecznie (pamiętaj, żeby zamknąć wszystkie miejsca, gdzie chcesz uzyskać dostęp do tego licznika, oczywiście). Zapobiega to wykonywaniu zablokowanego kodu przez inne wątki. Zapobiega też problemowi sekwencjonowania instrukcji wieloprocesorowego, o którym wspomniał wcześniej. Problem polega na tym, że blokada działa wolno, a jeśli używasz blokady w innych niepowiązanych miejscach, może to zablokować inne wątki.
Najlepsze
Interlocked.Increment (ref this.counter);
To bezpieczne i bardzo efektywne. Wykonuje trzy operacje odczytu, zwiększania, zapisu w jednym atomie bez przerwania w środku. Ponieważ nie wpływa to na inne kody, nie musisz pamiętać o zamkach gdzie indziej. Jest też bardzo szybki (jak mówi MSDN, na dzisiejszych procesorach często jest to tylko instrukcja).
Ale nie jestem do końca pewien, czy może to też rozwiązać problem kolejności instrukcji procesora, czy też trzeba go używać razem z Volatile i tym przyrostem.
Suplement: Jakie problemy lotne rozwiązują dobrze?
Ponieważ Volatile nie zapobiega wielowątkowości, co może zrobić? Dobrym przykładem są dwa wątki, jeden zawsze zapisuje do zmiennej, powiedzmy, że ta zmienna to queneLength, a drugi zawsze odczytuje dane z tej zmiennej. Jeśli QueueLongt nie jest ulotna, wątek A może odczytać 5 razy, ale wątek B może zobaczyć opóźnione dane lub nawet dane w niewłaściwej kolejności. Jednym z rozwiązań jest użycie blokady, ale w tym przypadku można też użyć lotności. Zapewnia to, że wątek B zawsze widzi najnowsze dane zapisane przez wątek A, ale ta logika działa tylko wtedy, gdy nie czytasz ich podczas zapisu i jeśli nie zapisujesz ich podczas odczytu. Gdy wiele wątków chce wykonać operacje czytania-modyfikowania-zapisu, trzeba użyć Interlocked lub Lock.
|