Zważywszy, że m
jest zmienna typu std::mutex
:
Wyobraźmy sobie tę sekwencję:
int a;
m.lock();
b += 1;
a = b;
m.unlock();
do_something_with(a);
istnieje 'oczywiste' rzecz dzieje się tutaj:
przyznaniem od B a przyrost b jest "chroniony" przed zakłóceniami z innych wątków, ponieważ inne wątki będą próbowały zablokować to samo m
i będą zablokowane, dopóki nie zadzwonimy pod numer m.unlock()
.
I dzieje się coś bardziej subtelnego.
W kodzie jednowątkowym kompilator będzie próbował ponownie zamówić ładunki i magazyny. Bez zamków, kompilator będzie wolny, aby skutecznie ponownie napisać kod, jeśli ten okazał się bardziej skuteczny na chipsecie:
int a = b + 1;
// m.lock();
b = a;
// m.unlock();
do_something_with(a);
lub nawet:
do_something_with(++b);
Jednakże std::mutex::lock()
, unlock()
, std::thread()
, std::async()
, std::future::get()
i tak dalej są ogrodzenia. Kompilator "wie", że nie może zmienić kolejności ładunków i zapisów (odczytuje i zapisuje) w taki sposób, że operacja kończy się po drugiej stronie ogrodzenia od miejsca, w którym podałeś swój kod.
1:
2: m.lock(); <--- This is a fence
3: b += 1; <--- So this load/store operation may not move above line 2
4: m.unlock(); <-- Nor may it be moved below this line
Wyobraźmy sobie, co by się stało, gdyby to nie był przypadek:
(kod kolejność)
thread1: int a = b + 1;
<--- Here another thread precedes us and executes the same block of code
thread2: int a = b + 1;
thread2: m.lock();
thread2: b = a;
thread2: m.unlock();
thread1: m.lock();
thread1: b = a;
thread1: m.unlock();
thread1:do_something_with(a);
thread2:do_something_with(a);
Jeśli zastosujemy ją poprzez, zobaczysz, że b ma teraz niewłaściwym wartość w nim, ponieważ kompilator był związany, aby twój kod był szybszy.
... i to tylko optymalizacje kompilatora. std::mutex
itp. Zapobiega również zmianie kolejności pamięci i pamięci w pamięciach w sposób bardziej "optymalny", co byłoby w porządku w środowisku jednowątkowym, ale katastrofalne w systemie wielordzeniowym (tj. Dowolnym nowoczesnym komputerze lub telefonie).
Koszt bezpieczeństwa jest taki, ponieważ pamięć podręczna wątku A musi zostać przepłukana, zanim wątek B odczyta te same dane, a przepłukanie pamięci podręcznych w pamięci jest ohydnie wolne w porównaniu z dostępem do pamięci podręcznej. Ale c'est la vie. Jest to jedyny sposób na zapewnienie bezpieczeństwa równoczesnej realizacji.
Dlatego wolimy, jeśli to możliwe, w systemie SMP, każdy wątek ma swoją własną kopię danych do pracy. Chcemy zminimalizować nie tylko czas spędzony w zamku, ale także liczbę przekroczeń przez płot.
Mogę porozmawiać o modyfikatorach std::memory_order
, ale jest to mroczna i niebezpieczna dziura, którą eksperci często wpadają w błąd, a początkujący nie mają żadnej nadziei na jej poprawne wykonanie.
muteks nie blokuje żadnych zmiennych, a jedynie blokuje wątek, który próbuje go uzyskać.Efektem jest ochrona zmiennych, jeśli program ma do nich dostęp tylko pomiędzy wywołaniem 'lock()' a 'unlock()'. Obecność mutex 'lock()' i 'unlock()' w kodzie uniemożliwia również kompilatorowi ponowne zamówienie dostępu do pamięci zmiennej * accesss * w ten sposób chronionej. –
Należy również upewnić się, że wątki używają tego samego obiektu mutex. Posiadanie różnych muteksów w każdym wątku nic nie da. Zamiast tego blokuje, jeśli inny wątek próbuje zablokować już zablokowany muteks, jak opisany @RichardHodges. – Hayt
Dzięki, naprawdę to wyczyściłem! – niko