Napisałem proste programy wielowątkowe w następujący sposób:
static bool finished = false;
int func()
{
size_t i = 0;
while (!finished)
++i;
return i;
}
int main()
{
auto result=std::async(std::launch::async, func);
std::this_thread::sleep_for(std::chrono::seconds(1));
finished=true;
std::cout<<"result ="<<result.get();
std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}
Zachowuje się normalnie w trybie debugowania w Visual Studio lub -O0
w gc c i drukuje wynik po 1
kilku sekundach. Ale utknął i nie drukuje niczego w trybie Release lub -O1 -O2 -O3
.
c++
multithreading
thread-safety
data-race
sz ppeter
źródło
źródło
Odpowiedzi:
Dwa wątki, uzyskujące dostęp do nieatomowej, niechronionej zmiennej to UB Dotyczy to
finished
. Można zrobićfinished
typustd::atomic<bool>
, aby to naprawić.Moja poprawka:
Wynik:
Demo na żywo na coliru
Ktoś może pomyśleć: „To
bool
- prawdopodobnie trochę. Jak to może być nieatomowe? (Zrobiłem to, kiedy sam zacząłem z wielowątkowością.)Ale zauważ, że brak łez nie jest jedyną rzeczą, która
std::atomic
ci daje. Umożliwia także dobrze zdefiniowany dostęp do odczytu i zapisu z wielu wątków, uniemożliwiając kompilatorowi zakładanie, że ponowne czytanie zmiennej zawsze zobaczy tę samą wartość.Wykonanie
bool
niestrzeżonego, nieatomowego atomu może powodować dodatkowe problemy:atomic<bool>
zememory_order_relaxed
sklepu / obciążenie będzie działać, ale gdzievolatile
nie. Za pomocą niestabilna w tym przypadku byłaby UB, nawet jeśli działa w praktyce na rzeczywistych implementacjach C ++.)Aby temu zapobiec, należy wyraźnie powiedzieć kompilatorowi, aby tego nie robił.
Jestem trochę zaskoczony ewoluującą dyskusją dotyczącą potencjalnego związku
volatile
tego problemu. Dlatego chciałbym wydać dwa centy:źródło
func()
i pomyślałem: „Mogę to zoptymalizować”. Optymalizator wcale nie dba o nici i wykryje nieskończoną pętlę i z przyjemnością zamieni ją w „chwilę” (prawda) „Jeśli spojrzymy na godbolta .org / z / Tl44iN możemy to zobaczyć. Jeśli skończoneTrue
, zwraca. Jeśli nie, przechodzi w bezwarunkowy skok do siebie (nieskończona pętla) w wytwórni.L5
volatile
w C ++ 11, ponieważ możesz uzyskać identyczny asm zatomic<T>
istd::memory_order_relaxed
. Działa to jednak na prawdziwym sprzęcie: pamięci podręczne są spójne, więc instrukcja ładowania nie może czytać przestarzałej wartości, gdy sklep na innym rdzeniu podejmie tam buforowanie. (MESI)volatile
jest jednak nadal UB. Naprawdę nigdy nie powinieneś zakładać, że coś, co jest zdecydowanie i wyraźnie UB jest bezpieczne tylko dlatego, że nie możesz wymyślić, w jaki sposób mogłoby się nie udać i zadziałało, gdy próbowałeś. To powoduje, że ludzie palą się w kółko.finished
za pomocąstd::mutex
działa (bezvolatile
lubatomic
). W rzeczywistości możesz zastąpić wszystkie atomiki „prostą” wartością + schematem mutex; nadal działałoby i działało wolniej.atomic<T>
wolno używać wewnętrznego muteksu;atomic_flag
gwarantuje się tylko bez blokady.Odpowiedź Scheffa opisuje, jak naprawić twój kod. Pomyślałem, że dodam trochę informacji o tym, co faktycznie dzieje się w tej sprawie.
Skompilowałem Twój kod na Godbolt, używając poziomu optymalizacji 1 (
-O1
). Twoja funkcja kompiluje się tak:Co się tu dzieje? Po pierwsze, mamy porównanie:
cmp BYTE PTR finished[rip], 0
- to sprawdza, czyfinished
jest fałszywe, czy nie.Jeśli nie jest to fałsz (inaczej prawda), powinniśmy wyjść z pętli przy pierwszym uruchomieniu. To osiągnąć przez
jne .L4
który j umps gdy n ot e qual do etykiety.L4
, gdy wartośći
(0
) przechowywana jest w rejestrze do późniejszego wykorzystania i powrót funkcji.Jeśli jednak jest to fałsz, przechodzimy do
Jest to bezwarunkowy skok,
.L5
którego etykietą jest tak naprawdę samo polecenie skoku.Innymi słowy, wątek jest umieszczany w nieskończonej zajętej pętli.
Dlaczego to się stało?
Jeśli chodzi o optymalizator, wątki są poza jego zakresem. Zakłada, że inne wątki nie odczytują ani nie zapisują zmiennych jednocześnie (ponieważ byłoby to UB wyścigu danych). Musisz powiedzieć, że nie może zoptymalizować dostępu. Właśnie tutaj pojawia się odpowiedź Scheffa. Nie zawracam sobie głowy powtórzeniem go.
Ponieważ optymalizatorowi nie powiedziano, że
finished
zmienna może potencjalnie ulec zmianie podczas wykonywania funkcji, widzi, żefinished
nie jest modyfikowana przez samą funkcję i zakłada, że jest stała.Zoptymalizowany kod zapewnia dwie ścieżki kodu, które będą wynikały z wejścia do funkcji ze stałą wartością bool; albo uruchamia pętlę nieskończenie, albo pętla nigdy nie jest uruchamiana.
w
-O0
kompilatorze (zgodnie z oczekiwaniami) nie optymalizuje treści pętli i porównuje:dlatego funkcja, gdy niezoptymalizowana działa, brak atomowości tutaj zwykle nie stanowi problemu, ponieważ kod i typ danych są proste. Prawdopodobnie najgorsze, na co moglibyśmy się tutaj natknąć, to wartość,
i
która nie jest zgodna z tym, co powinno być.Bardziej złożony system ze strukturami danych znacznie bardziej prawdopodobne jest uszkodzenie danych lub nieprawidłowe wykonanie.
źródło
atomic
zmiennych niezmiennych w kodzie, który nie zapisuje tych zmiennych. np.if (cond) foo=1;
nie można go przekształcić w asm,foo = cond ? 1 : foo;
ponieważ jest tak, ponieważ ładunek + magazyn (nie atomowa RMW) może nadepnąć na zapis z innego wątku. Kompilatory już tego unikały, ponieważ chciały być przydatne do pisania programów wielowątkowych, ale C ++ 11 oficjalnie nakazało, aby kompilatory nie łamały kodu, w którym piszą 2 wątkia[1]
ia[2]
Ze względu na kompletność w krzywej uczenia się; powinieneś unikać używania zmiennych globalnych. Wykonałeś dobrą robotę, ustawiając go na statyczny, więc będzie on lokalny dla jednostki tłumaczącej.
Oto przykład:
Na żywo w Wandbox
źródło
finished
jakostatic
w bloku funkcyjnym. Nadal będzie inicjalizowany tylko raz, a jeśli zostanie zainicjowany na stałą, nie wymaga to blokowania.finished
może również wykorzystywać tańszestd::memory_order_relaxed
ładunki i sklepy; nie ma wymaganego wrt zamówienia. inne zmienne w każdym wątku. Nie jestem pewien sugestii @ Davislorstatic
ma sens; jeśli masz wiele wątków z liczbą obrotów, nie musisz zatrzymywać ich wszystkich z tą samą flagą. Chcesz napisać inicjalizacjęfinished
w sposób, który kompiluje się do samej inicjalizacji, a nie do magazynu atomowego. (Podobnie jak w przypadkufinished = false;
domyślnej składni inicjalizującej C ++ 17. Godbolt.org/z/EjoKgq ).