2009-07-09 9 views
8

Pochodzę w dużej mierze z tła C++, ale myślę, że to pytanie dotyczy wątków w dowolnym języku. Oto scenariusz:W jaki sposób systemy gwintowane radzą sobie z udostępnianiem danych w pamięci podręcznej przez inne cpus?

  1. Mamy dwa wątki (ThreadA i ThreadB), a wartość x w pamięci współdzielonej

  2. Załóżmy, że dostęp do x jest odpowiednio kontrolowana przez mutex (lub innej odpowiedniej kontroli synchronizacji)

  3. Jeśli wątki działają na różnych procesorach, co się stanie, jeśli ThreadA wykona operację zapisu, ale procesor umieści wynik w pamięci podręcznej L2 zamiast w pamięci głównej? Następnie, jeśli ThreadB spróbuje odczytać wartość, czy nie będzie tylko wyglądał w swojej własnej pamięci podręcznej L1/L2/pamięci głównej, a następnie będzie pracował ze starą wartością?

Jeśli tak nie jest, to w jaki sposób zarządza się tym problemem?

Jeśli tak, to co można z tym zrobić?

Odpowiedz

9

Twój przykład będzie działał dobrze.

Wiele procesorów korzysta z coherency protocol, takich jak MESI, aby zapewnić synchronizację danych między pamięcią podręczną. W przypadku MESI każda linia pamięci podręcznej jest uważana za zmodyfikowaną, utrzymywaną wyłącznie, współdzieloną między procesorami lub niepoprawną. Zapisywanie linii pamięci podręcznej współużytkowanej między procesorami zmusza ją do unieważnienia w innych procesorach, utrzymując synchronizacje w pamięci podręcznej.

To jednak nie wystarczy. Różne procesory mają różne memory models, a najnowsze procesory obsługują pewien poziom ponownego zamawiania pamięci. W takich przypadkach potrzebne są memory barriers.

Na przykład, jeśli masz Temat A:

DoWork(); 
workDone = true; 

i nici B:

while (!workDone) {} 
DoSomethingWithResults() 

Z obu działa na oddzielnych procesorów, nie ma gwarancji, że zapisy wykonane w ciągu DoWork() będzie być widoczne dla wątku B, zanim zapis do workDone i DoSomethingWithResults() będzie przebiegał z potencjalnie niespójnym stanem. Bariery pamięciowe gwarantują pewne porządkowanie odczytów i zapisów - dodanie do zapory pamięci po DoWork() w wątku A zmusiłoby wszystkie zapisy/zapisy zrobione przez DoWork do ukończenia przed zapisaniem do workDone, aby wątek B uzyskał spójny widok. Muteksy z natury zapewniają barierę pamięci, dzięki czemu odczyt/zapis nie może przekazać połączenia w celu zablokowania i odblokowania.

W twoim przypadku jeden procesor zasygnalizowałby innym, że zabrudził linię pamięci podręcznej i zmusił inne procesory do przeładowania z pamięci. Uzyskanie muteksu do odczytu i zapisu wartości gwarantuje, że zmiana w pamięci jest widoczna dla drugiego procesora w oczekiwanej kolejności.

+0

Dziękuję bardzo za tę odpowiedź. Zastanawiałem się, czy w grę wchodzi tu jakiś sprzętowy mechanizm poziomu, ponieważ wydawało się, że istnieją praktyczne granice tego, co można osiągnąć na poziomie języka/kompilatora. – csj

1

Większość prymitywów blokujących, takich jak muteksy, implikuje memory barriers. Spowoduje to wymuszenie płukania pamięci podręcznej i ponowne załadowanie.

Przykładowo

ThreadA { 
    x = 5;   // probably writes to cache 
    unlock mutex; // forcibly writes local CPU cache to global memory 
} 
ThreadB { 
    lock mutex; // discards data in local cache 
    y = x;   // x must read from global memory 
} 
+1

Nie wierzę, że bariery wymuszają spłukiwanie pamięci podręcznej, wymuszają ograniczenia na kolejności operacji związanych z pamięcią. Spłukanie pamięci podręcznej nie pomoże, jeśli zapis do X może zakończyć odblokowywanie muteksu. – Michael

+0

Bariery byłyby całkiem bezużyteczne, gdyby kompilator ponownie zamawiał operacje pamięciowe na nich, hmm? Przynajmniej dla GCC, myślę, że jest to zwykle realizowane za pomocą clobbera pamięci, który mówi GCC "unieważnia wszelkie założenia dotyczące pamięci". – ephemient

+0

Och, widzę, co mówisz. Bufor pamięci podręcznej nie jest konieczny, o ile porządek jest właściwie przestrzegany między procesorami. Więc przypuszczam, że to wyjaśnienie jest uproszczonym widokiem, a twoje bardziej zagłębia się w szczegóły sprzętu. – ephemient

0

Ogólnie, kompilator rozumie wspólna pamięć i ma znaczny wysiłek, aby zapewnić, że wspólna pamięć jest umieszczony w łatwością udostępniać miejscu. Współczesne kompilatory są bardzo skomplikowane w sposobie, w jaki zamawiają operacje i dostęp do pamięci; mają tendencję do rozumienia natury wątków i wspólnej pamięci. Nie oznacza to, że są one doskonałe, ale ogólnie rzecz biorąc, wiele uwagi poświęca kompilator.

0

C# ma pewne wbudowane wsparcie dla tego rodzaju problemów. Możesz oznaczyć zmienną słowem kluczowym volatile, które wymusza synchronizację na wszystkich procesorach.

public static volatile int loggedUsers; 

Druga część jest składniowej wrappper wokół metod zwanych Threading.Monitor.Enter NET (x) i Threading.Monitor.Exit (x), gdzie x jest zmienna w celu zablokowania. Powoduje to, że inne wątki próbują zablokować x, aby musiały czekać aż do wątku blokującego wywołań Exit (x).

public list users; 
// In some function: 
System.Threading.Monitor.Enter(users); 
try { 
    // do something with users 
} 
finally { 
    System.Threading.Monitor.Exit(users); 
} 
Powiązane problemy