2013-10-30 16 views
11

Nawet na prosty 2-wątku przykład komunikacji, mam trudność wyrazić to w C11 atomowej i memory_fence stylu w celu uzyskania odpowiedniej pamięci Kolejność:C11 płot pamięć Wykorzystanie

wspólne dane:

volatile int flag, bucket; 
wątek

producent:

while (true) { 
    int value = producer_work(); 
    while (atomic_load_explicit(&flag, memory_order_acquire)) 
     ; // busy wait 
    bucket = value; 
    atomic_store_explicit(&flag, 1, memory_order_release); 
} 

konsument thre ad:

while (true) { 
    while (!atomic_load_explicit(&flag, memory_order_acquire)) 
     ; // busy wait 
    int data = bucket; 
    atomic_thread_fence(/* memory_order ??? */); 
    atomic_store_explicit(&flag, 0, memory_order_release); 
    consumer_work(data); 
} 

O ile mi zrozumieć, że powyższy kod poprawnie zamówić sklep-w-wiaderku -> flag-sklep -> Flaga obciążenia -> load-z-wiadra. Uważam jednak, że nadal istnieje warunek wyścigowy między ładowaniem z zasobnika i ponownym zapisywaniem wiadra z nowymi danymi. Aby wymusić zamówienie po przeczytaniu wiadra, domyślam się, że potrzebuję jawnego atomic_thread_fence() między odczytem wiadra i następującą stacją atomową. Niestety, wydaje się, że nie ma argumentu, aby wymusić cokolwiek na poprzednich ładunkach, nawet na memory_order_seq_cst.

Naprawdę zabrudzonym rozwiązaniem może być ponowne przypisanie bucket w wątku konsumenckim z fałszywą wartością: jest to sprzeczne z pojęciem "tylko do odczytu".

W starszym C99/GCC świecie mógłbym użyć tradycyjnego __sync_synchronize(), który moim zdaniem byłby wystarczająco silny.

Jakie byłoby ładniejsze rozwiązanie w stylu C11 do synchronizowania tej tak zwanej anty-zależności?

(Oczywiście zdaję sobie sprawę, że powinienem lepiej unikać takiego kodowanie niskopoziomowe i wykorzystywać dostępne konstrukcje wyższego poziomu, ale chciałbym zrozumieć ...)

+1

Nie jestem programistą C++, ale (koncepcyjnie) nie jestem pewien, czy konieczne jest wywołanie 'atomic_thread_fence()'. Aktualizacja flagi ma semantykę wyzwalającą, zapobiegając przeporządkowaniu wszystkich poprzednich instrukcji sklepu (np. Do "danych"). Zapis do 'data' ma zależność od odczytu z' bucket', więc odczyt nie może zostać ponownie uporządkowany po wydaniu flagi. Jeśli konieczne jest pełne ogrodzenie, bardzo chciałbym usłyszeć dlaczego. –

+2

Brak odpowiedzi, a więc tylko komentarz: wydaje się, że ponownie wynajduje się typ danych "atomowej_flagi" C11, który implementuje dokładnie to semantyczne, ale które ostatecznie ma bardziej bezpośrednią implementację w sprzęcie. 'atomic_flag' to jedyny atomowy typ danych, który gwarantuje, że jest wolny od blokady, więc zawsze jest to preferowane w przypadku bardziej złożonych operacji. I definitywnie nie będzie potrzebował dodatkowego ogrodzenia, aby zapewnić spójność. –

+0

Mike S, twoja odpowiedź wydaje mi się atrakcyjna, ale ... Myślałem, że wspomnienia z pamięci to zapewnienie rzeczy w podsystemie pamięci, wpływając na ld/st ops. W powyższym przykładzie "dane" prawdopodobnie staną się zmienną rejestrową, więc jej przydział nie tworzy sklepu op. To by tylko pozostawiło ładunek z łyżki do synchronizacji pamięci? (dla którego nie ma kolejnej kolejności pamięci C11?) –

Odpowiedz

3

Aby wymusić zamówienie po przeczytaniu zasobnika, domyślam się, że potrzebowałbym jawnej wartości atrybutu atomic_thread_fence() między odczytanym zbiornikiem a następującym plikiem atomowym.

Nie wierzę wywołanie atomic_thread_fence() jest konieczne: aktualizacja flaga ma semantykę uwalnianiu, zapobieganie wszelkim poprzednich obciążeń lub sklepu operacji przed kolejność poprzek. Zobacz formalnej definicji przez Herb Sutter:

Odpisy uwalnianiu wykonuje przecież czyta i pisze przez tego samego wątku, które poprzedzają je w celu programu.

To powinno zapobiec lektury bucket przed kolejność wystąpić po aktualizacji flag, niezależnie od tego, gdzie kompilator zdecyduje się zapisać data.

To prowadzi mnie do komentarz o innej odpowiedzi:

volatile zapewnia, że ​​nie są ld/st generowane operacje, które mogą być następnie zamówić z ogrodzenia. Dane są jednak zmienną lokalną, a nie zmienną. Prawdopodobnie kompilator umieści go w rejestrze, unikając operacji przechowywania. To pozostawia ładunek z kubła, który ma zostać zamówiony, z późniejszym resetem flagi.

Wydaje się, że nie jest to problem, jeśli odczyt bucket nie mogą zostać zreorganizowane obok zapisu uwalnianiu flag, więc volatile nie powinno być konieczne (choć prawdopodobnie nie zaszkodzi go mieć, albo). Jest to również niepotrzebne, ponieważ większość wywołań funkcji (w tym przypadku atomic_store_explicit(&flag)) służy jako bariery pamięci podczas kompilacji. Kompilator nie zmienia kolejności odczytu zmiennej globalnej poza nieliniowym wywołaniem funkcji, ponieważ ta funkcja może modyfikować tę samą zmienną.

Zgadzam się również z @MaximYegorushkin, że możesz poprawić swoje intensywne czekanie z instrukcjami pause podczas kierowania na kompatybilne architektury. GCC i ICC wydają się mieć wewnętrzne właściwości _mm_pause(void) (prawdopodobnie równoważne __asm__ ("pause;")).

+0

Dziękuję Mike za poprawne zapisanie. Więc potwierdzasz, że dzisiejszy opis 'memory_order_release' na stronie cppreference.com jest niepoprawny (twierdząc, że działa tylko na poprzednie sklepy). Jeśli chodzi o 'volatile' dla' bucket': jeśli kompilator miałby wiedzę na temat 'atomic_store_explicit' i wbudowałby wywołanie, to nieulotna łyżka mogłaby zostać zmapowana, aby pominąć niektóre z zamierzonych obciążeń i wymian danych między wątkami? –

+0

To może nie być _nie poprawne_ jako takie; technicznie rzecz biorąc, Sutter opisywał semantykę "write-release", którą chciałbym wziąć pod uwagę "** a write ** z semantyką release", np. 'atomic_store_explicit()'. To może być po prostu silniejsza gwarancja niż ta zapewniana przez samotną barierę zwalniającą, np. 'Atomic_thread_fence()', w którym to przypadku dokumentacja dla 'memory_order_release' może po prostu opisywać _minimal_ gwarancje. Niestety, uzyskanie autorytatywnych odpowiedzi na tego typu rzeczy stało się niezwykle trudne, ale każda definicja "semantyki wydawniczej", którą widziałem, zgadza się z Sutter. –

+0

Zaakceptowanie tej odpowiedzi oznacza, że ​​witryna cppreference.com jest nieprawidłowa. @Mike, powyższy link do preshing.com również oznacza to. Właśnie znalazłem http://stackoverflow.com/questions/16179938, gdzie odpowiedź stwierdza również, że cppreference.com jest źle: obie mówią, że store.release powinien zamówić na poprzednich ładowaniach. Jednak projekt C++ 11 stycznia 2012 mówi: "Nieformalnie, wykonując operację zwolnienia na siłach A poprzednia strona wpływa na inne lokalizacje pamięci, aby stały się widoczne dla innych wątków". Te "efekty uboczne" teraz wykluczają odczyty? To osłabia możliwość zmiany "memory_order_release" na cppreference.com :-( –

1

zgadzam się z tym, co mówi w @MikeStrobel jego komentarz.

Nie potrzebujesz tutaj atomic_thread_fence(), ponieważ sekcje krytyczne zaczynają się od nabycia i kończą semantyką wydania. W związku z tym, odczytów w kluczowych sekcjach nie można zmienić przed nabyciem i pisze po wydaniu. I dlatego tutaj również niepotrzebne jest volatile.

Ponadto nie widzę powodu, dla którego zamiast tego użyty został spinlock (pthread). Spinlock robi podobną zajęty zawrót dla Ciebie, ale również wykorzystuje pause instruction:

Pauza wewnętrzna służy w spin-czekać pętle z procesorami wykonawczych dynamiczne wykonanie (zrealizowanie szczególnie out-of-order). W pętli spin-wait, przerwa wewnętrzna poprawia szybkość, z jaką kod wykrywa zwolnienie blokady i zapewnia szczególnie znaczny wzrost wydajności. Wykonanie następnej instrukcji jest opóźnione o określoną ilość czasu. Instrukcja PAUSE nie modyfikuje stanu architektonicznego. W przypadku dynamicznego planowania instrukcja PAUSE zmniejsza karę wyjścia z pętli spinów.

+0

Jeśli 'bucket' nie zostanie zadeklarowany jako' volatile', wątpię, czy ładunki z 'bucket' zawsze zwracają właściwą wartość: The volatile wydaje się potrzebny, aby uniemożliwić kompilatorowi zrobienie lokalnej kopii rejestru' bucket'. W rzeczy samej nie wiem, czy "lotny" jest nadal potrzebny do oznaczenia flagi, to wewnętrzne atomy mogą mieć zasięg. Należy jednak zauważyć, że prototypy C11 tych funkcji określają argument "volatile". –

+1

@JosvE Nope, 'volatile' jest niepotrzebny, ponieważ masz bariery pamięci przed i po uzyskaniu do niego dostępu, to wszystko, co ważne. Zobacz http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic- Weapons-1--2-2, a druga część zawiera szczegółowe omówienie Twojego pytania i również lotny. –

+1

OK, zagłębiłem się w prezentację Sutters ... Masz rację, że kompilator musi zadbać o zamawianie z ogrodzeniem atomowym, bez potrzeby dalszych "lotnych" deklaracji. Dzięki. –

-1

odpowiedź Direct:

że sklep jest operacja memory_order_release oznacza, że ​​kompilator musi emitować ogrodzenie pamięci do przechowywania instrukcji przed sklepie flagi.Jest to wymagane, aby zapewnić, że inne procesory widzą ostateczny stan opublikowanych danych, zanim zaczną je interpretować. Nie, nie musisz dodawać drugiego ogrodzenia.


odpowiedź Long:

Jak wspomniano powyżej, co się dzieje, jest to, że kompilator przekształca swoje instrukcje atomic_... w kombinacji ogrodzeń i uzyskuje dostęp do pamięci; podstawową abstrakcją nie jest ładunek atomowy, to płot pamięci. Tak to działa, mimo że nowe abstrakcje w C++ zachęcają do myślenia inaczej. Osobiście uważam, że znacznie łatwiej jest myśleć o płotach pamięciowych niż o pokrzywdzonych abstrakcjach w C++.

Z punktu widzenia sprzętu, co musisz zapewnić, jest względne zamówienie Twoich ładunków i zapasów, i. mi. że zapis do zasobnika kończy się, zanim flaga zostanie zapisana w producencie, a obciążenie flagi odczytuje wartość starszą niż ładunek wiadra w konsumentach.

Mimo to, czego faktycznie potrzebujesz to:

//producer 
while(true) { 
    int value = producer_work(); 
    while (flag) ; // busy wait 
    atomic_thread_fence(memory_order_acquire); //ensure that value is not assigned to bucket before the flag is lowered 
    bucket = value; 
    atomic_thread_fence(memory_order_release); //ensure bucket is written before flag is 
    flag = true; 
} 

//consumer 
while(true) { 
    while(!flag) ; // busy wait 
    atomic_thread_fence(memory_order_acquire); //ensure the value read from bucket is not older than the last value read from flag 
    int data = bucket; 
    atomic_thread_fence(memory_order_release); //ensure data is loaded from bucket before the flag is lowered again 
    flag = false; 
    consumer_work(data); 
} 

Uwaga, że ​​etykiety „producent” i „konsument” są mylące tutaj, bo mamy dwa procesy gra ping-pong, każdy producent staje i konsument z kolei; to tylko jeden wątek wytwarza użyteczne wartości, podczas gdy drugi wytwarza "dziury", aby zapisać przydatne wartości w ...

atomic_thread_fence() to wszystko, czego potrzebujesz, a ponieważ jest to bezpośrednio przekłada się na instrukcje asemblera poniżej abstrakcji atomic_..., to gwarantowane najszybsze podejście.

+0

Bez zblokowanych (atomowych) odczytów 'flag', nie oczekiwałbym odczytu ostatniej wartości' flag', jeśli producent i konsument działają na oddzielnych procesorach. Aktualizacje można zamówić, ale nowe wyniki mogą nie być _seen_. Spodziewałbym się, że potrzebuje on również sklepu z blokadą podczas aktualizacji flagi, z podobnych powodów. Co najmniej, zwolnienia ogrodzeń powinny następować po "przydziałach flagowych". –

+0

@MikeStrobel Nie, wersje muszą znajdować się przed przypisaniami do flag, są to ogrodzenia magazynów: każdy sklep wcześniej musi stać się skuteczny przed jakimkolwiek sklepem po nim. Odnośnie atomowości: Tak, przypisania do flagi muszą być atomowe, ale pokaż mi architekturę, w której magazyn do właściwie wyrównanego 'int' nie jest atomowy. Nie potrzebujemy do tego żadnego wsparcia językowego. – cmaster

+0

whoops, tak, lekceważcie to, co powiedziałem o przeniesieniu ogrodzenia wydania po sklepie. W odniesieniu do atomów nie chodzi jednak o to, że odczyty i zapisy do "flag" nie są atomowe, ale że nie są zblokowane (i przypuszczalnie atomowe funkcje odczytu/zapisu w tym API są). W twojej wersji, w jaki sposób zapewnia się, że inne jednostki centralne nie próbują odczytać nieaktualnej wersji "flagi" z lokalnej pamięci podręcznej? –