2016-09-07 46 views
14

Czy kompilator po prostu sprawdzić, które zmienne są modyfikowane pomiędzy zamkiem i odblokować oświadczeń i związać je do mutex więc nie ma wyłączny dostęp do nich?W jaki sposób C++ std :: mutex łączy się z zasobem?

Albo robi mutex.lock() zablokować wszystkie zasoby, które są widoczne w bieżącym zakresie?

+13

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. –

+3

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

+0

Dzięki, naprawdę to wyczyściłem! – niko

Odpowiedz

15

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.

+4

Twoje ostatnie zdanie jest niedopowiedzeniem. :) – Mysticial

1

Pod względem wykorzystania zasobów, kompilator nie zalezalo na mutex.lock(). Obowiązkiem programistów jest zapewnienie dostępu do zasobów w ramach właściwego blokowania/odblokowywania.

Jednak kompilatorowi zależy na muteksie w taki sposób, że wie, że nie będzie optymalizował niektórych konstrukcji, które byłyby w innym przypadku - ale prawdopodobnie nie jesteś zainteresowany tym teraz.

6

"mutex" to skrót od "wzajemnego wykluczania"; Właściwą dyscypliną do korzystania z muteksa jest zablokowanie go przed wprowadzeniem dowolnego kodu, który modyfikuje zmienne, które są współużytkowane przez wątki i do odblokowania go, gdy ta sekcja kodu zostanie wykonana. Jeśli jeden wątek zablokuje muteks, każdy inny wątek, który spróbuje go zablokować, zostanie zablokowany, dopóki wątek będący jego właścicielem nie odblokuje go. Oznacza to, że tylko jeden wątek na raz jest wewnątrz kodu, który może modyfikować wspólne zmienne, a to eliminuje warunki wyścigu.

Reszta tego, co muteks opiera się na magii kompilatora na pewnym poziomie: zapobiega również przenoszeniu przez kompilator danych i ładunków z wewnątrz chronionego kodu na zewnątrz i na odwrót, co jest konieczne dla chronionych kod, aby pozostać chroniony.

3

Mutex szczególna realizacja semaphore. W szczególności jest to binarny semafor.

Semafor (oczywiście w kontekście informatyki) mogą być realizowane jako zmiennej całkowitej i sprzętu lub oprogramowania (system operacyjny) prymitywny które są atomowy (nie może być przerwana).

Imagine coś jak (kod poziomu pseudo-wys):

int mutex = 1; // The mutex is free when created (free=1, occupied=0). 

// in a concurrency block 
{ 
    :try-again 

    // test if mutex is 1 (is free). 
    if (mutex > 0) { 

    // mutex WAS free so now I set as occupied (set 0) 
    --mutex; 
    // Now I've 'acquired' the mutex and since the mutex is 0 or 1 
    // only me can be here. 

    // Do something in mutual exclusion 
    // Done. 
    // Unlock the mutex 
    ++mutex; 
    // Now the mutex is free so other threads can acquire it. 

    } else { 
    // Mutex is 0 so I tried but it's already occupied. 
    // Block the thread and try again. 
    goto try-again; 
    } 

} 

oczywiste w czystym języku wysokiego poziomu, że podejście to nie może działać, ponieważ wątek może zostać przerwany po przetestowała mutex jest wolne i zanim będzie można ustawić jako zajęte.

Z tego powodu semafory i tak muteks są realizowane za pomocą prymitywnych instrukcji, które implementują operację "testuj i ustaw" w "jednym zegarze" (atomowo).

Przykładem jest prymitywum test-and-set.

+0

Ponieważ zostałem spalony przez traktowanie muteksu i binarnego semafora jako wymiennego, istnieje różnica. W wielu bibliotekach wątków (takich jak pthreads) muteks może zostać odblokowany tylko przez wątek, który je zablokował, podczas gdy semafor może zostać "odblokowany" przez dowolny wątek. –

3

Nie ma takiej sprytu, a poprawne działanie jest obowiązkiem programisty.

Mutex jest jak zamykane drzwi w domu, który nie ma ścian.

Wszystko, co możesz zrobić, to uniemożliwić innym wejście do domu przez drzwi po zamknięciu.
Drzwi są przydatne tylko wtedy, gdy wszyscy zgadzają się na wejście do domu przez drzwi, a gdy drzwi są zamknięte, należy poczekać, aż osoba w środku otworzy drzwi i wyjdzie.
Nic nie zabrania złemu człowiekowi wejścia do domu przez nieistniejące mury, za wyjątkiem umowy, której nie powinieneś.

+1

Sprytność polega na tym, że optymalizator kompilatora i generator kodu rozpoznają i respektują domniemane ogrodzenia pamięci. –

+0

Nieważne, że bez ścian nie masz zbyt dużego domu. – immibis