Słyszałem, że const
oznacza to bezpieczeństwo wątków w C ++ 11 . Czy to prawda?
To trochę prawda ...
Oto, co język standardowy ma do powiedzenia na temat bezpieczeństwa wątków:
[1.10 / 4]
Dwie oceny wyrażeń powodują konflikt, jeśli jedna z nich modyfikuje lokalizację pamięci (1.7), a druga uzyskuje dostęp lub modyfikuje tę samą lokalizację pamięci.
[1.10 / 21]
Wykonanie programu zawiera wyścig danych, jeśli zawiera dwie sprzeczne akcje w różnych wątkach, z których przynajmniej jeden nie jest atomowy i żadne nie występuje przed drugim. Każdy taki wyścig danych skutkuje niezdefiniowanym zachowaniem.
co jest niczym innym jak warunkiem wystarczającym do wystąpienia wyścigu danych :
- Na danej rzeczy są wykonywane jednocześnie dwie lub więcej czynności; i
- Przynajmniej jeden z nich to pismo.
Biblioteka standardowa opiera się na tym, idąc nieco dalej:
[17.6.5.9/1]
Ta sekcja określa wymagania, które muszą spełnić implementacje, aby zapobiec wyścigom danych (1.10). Każda standardowa funkcja biblioteczna spełnia każdy wymóg, chyba że określono inaczej. Implementacje mogą zapobiegać wyścigom danych w przypadkach innych niż określone poniżej.
[17.6.5.9/3]
Standardowa funkcja biblioteczna C ++ nie może bezpośrednio ani pośrednio modyfikować obiektów (1.10) dostępnych dla wątków innych niż bieżący wątek, chyba że dostęp do obiektów jest uzyskiwany bezpośrednio lub pośrednio za pośrednictwemargumentówinnych niż const , w tymthis
.
który w prostych słowach mówi, że oczekuje, że operacje na const
obiektach będą bezpieczne dla wątków . Oznacza to, że biblioteka standardowa nie wprowadzi wyścigu danych tak długo, jak operacje na const
obiektach własnego typu
- Składają się wyłącznie z lektur - to znaczy nie ma zapisów -; lub
- Wewnętrznie synchronizuje zapisy.
Jeśli to oczekiwanie nie dotyczy jednego z twoich typów, to użycie go bezpośrednio lub pośrednio razem z dowolnym elementem Biblioteki standardowej może spowodować wyścig danych . Podsumowując, const
oznacza bezpieczeństwo wątków z punktu widzenia biblioteki standardowej . Ważne jest, aby pamiętać, że jest to tylko umowa i nie będzie egzekwowana przez kompilator, jeśli ją złamiesz, otrzymasz niezdefiniowane zachowanie i jesteś sam. To, czy const
jest obecny, czy nie, wpłynie na generowanie kodu - przynajmniej nie w odniesieniu do wyścigów danych -.
Czy to znaczy, const
jest teraz odpowiednikiem Javy s” synchronized
?
Nie . Ani trochę...
Rozważmy następującą, nadmiernie uproszczoną klasę reprezentującą prostokąt:
class rect {
int width = 0, height = 0;
public:
/*...*/
void set_size( int new_width, int new_height ) {
width = new_width;
height = new_height;
}
int area() const {
return width * height;
}
};
Funkcja członkowska area
jest bezpieczna wątkowo ; nie dlatego const
, że jest, ale dlatego, że składa się wyłącznie z operacji odczytu. Nie ma żadnych zapisów, a przynajmniej jeden zapis jest potrzebny, aby wystąpił wyścig danych . Oznacza to, że możesz dzwonić area
z dowolnej liczby wątków i przez cały czas będziesz otrzymywać prawidłowe wyniki.
Zauważ, że nie oznacza rect
to, że jest bezpieczny dla wątków . W rzeczywistości łatwo jest zobaczyć, jak gdyby wywołanie area
miało nastąpić w tym samym czasie, co wywołanie set_size
danego rect
, to area
mogłoby zakończyć się obliczeniem jego wyniku na podstawie starej szerokości i nowej wysokości (lub nawet zniekształconych wartości) .
Ale to jest w porządku, rect
nie jest const
więc nawet oczekiwane, że będzie w końcu bezpieczne dla wątków . Z const rect
drugiej strony, zadeklarowany obiekt byłby bezpieczny dla wątków, ponieważ żadne zapisy nie są możliwe (a jeśli rozważasz - biorąc pod uwagę const_cast
coś pierwotnie zadeklarowanego const
, otrzymasz niezdefiniowane zachowanie i to wszystko).
Więc co to znaczy?
Załóżmy - ze względu na argumentację - że operacje mnożenia są niezwykle kosztowne i lepiej ich unikać, jeśli to możliwe. Moglibyśmy obliczyć obszar tylko wtedy, gdy jest żądany, a następnie buforować go na wypadek ponownego zażądania w przyszłości:
class rect {
int width = 0, height = 0;
mutable int cached_area = 0;
mutable bool cached_area_valid = true;
public:
/*...*/
void set_size( int new_width, int new_height ) {
cached_area_valid = ( width == new_width && height == new_height );
width = new_width;
height = new_height;
}
int area() const {
if( !cached_area_valid ) {
cached_area = width;
cached_area *= height;
cached_area_valid = true;
}
return cached_area;
}
};
[Jeśli ten przykład wydaje się zbyt sztuczny, można psychicznie zastąpić int
przez bardzo dużą dynamicznie przydzielonej liczby całkowitej , która jest z natury nie wątku bezpieczny i dla których mnożenia są niezwykle kosztowne.]
Funkcja członkowska area
nie jest już bezpieczna dla wątków , teraz wykonuje zapisy i nie jest wewnętrznie synchronizowana. To jest problem? Wywołanie do area
może nastąpić jako część konstruktora kopiującego innego obiektu, taki konstruktor mógł zostać wywołany przez jakąś operację na standardowym kontenerze iw tym momencie biblioteka standardowa oczekuje, że operacja ta będzie się zachowywać jak odczyt w odniesieniu do wyścigów danych . Ale my piszemy!
Gdy tylko umieścimy plik rect
w standardowym kontenerze - bezpośrednio lub pośrednio - zawieramy umowę z Biblioteką Standardową . Aby nadal wykonywać zapisy w const
funkcji, jednocześnie przestrzegając tego kontraktu, musimy wewnętrznie zsynchronizować te zapisy:
class rect {
int width = 0, height = 0;
mutable std::mutex cache_mutex;
mutable int cached_area = 0;
mutable bool cached_area_valid = true;
public:
/*...*/
void set_size( int new_width, int new_height ) {
if( new_width != width || new_height != height )
{
std::lock_guard< std::mutex > guard( cache_mutex );
cached_area_valid = false;
}
width = new_width;
height = new_height;
}
int area() const {
std::lock_guard< std::mutex > guard( cache_mutex );
if( !cached_area_valid ) {
cached_area = width;
cached_area *= height;
cached_area_valid = true;
}
return cached_area;
}
};
Zauważ, że sprawiliśmy, że area
funkcja jest bezpieczna wątkowo , ale rect
nadal nie jest bezpieczna wątkowo . Wezwanie do area
zdarzenia w tym samym czasie, w którym wywołanie set_size
może nadal kończyć się obliczeniem niewłaściwej wartości, ponieważ przypisania do width
i height
nie są chronione przez muteks.
Gdybyśmy naprawdę chcieli mieć bezpieczny rect
wątkowo, użylibyśmy operacji podstawowej synchronizacji do ochrony tego, co nie jest bezpieczne dla wątków rect
.
Czy kończą się słowa kluczowe ?
Tak, oni są. Od pierwszego dnia brakuje im słów kluczowych .
Źródło : nie wiesz const
imutable
- Herb Sutter