Multi-threaded executions and data races (seit C++11)
Ein Thread of execution ist ein Kontrollfluss innerhalb eines Programms, der mit dem Aufruf einer bestimmten Top-Level-Funktion beginnt (durch std::thread, std::async, std::jthread(seit C++20) oder andere Mittel), und rekursiv einschließlich jeder Funktionsaufrufe, die anschließend vom Thread ausgeführt werden.
- Wenn ein Thread einen anderen erstellt, wird der erste Aufruf der Top-Level-Funktion des neuen Threads vom neuen Thread ausgeführt, nicht vom erstellenden Thread.
Jeder Thread kann potenziell auf jedes Objekt und jede Funktion im Programm zugreifen
- Objekte mit automatischer und Thread-lokaler Speicherdauer können immer noch von einem anderen Thread über einen Zeiger oder per Referenz zugegriffen werden.
- Unter einer gehosteten Implementierung kann ein C++-Programm mehr als einen Thread gleichzeitig ausführen. Die Ausführung jedes Threads erfolgt wie auf dieser Seite beschrieben. Die Ausführung des gesamten Programms besteht aus der Ausführung aller seiner Threads.
- Unter einer freistehenden Implementierung ist es implementierungsabhängig, ob ein Programm mehr als einen Ausführungs-Thread haben kann.
Für einen Signal-Handler, der nicht als Ergebnis eines Aufrufs von std::raise ausgeführt wird, ist nicht spezifiziert, welcher Ausführungs-Thread die Signal-Handler-Ausführung enthält.
Inhalt |
[bearbeiten] Data races
Verschiedene Ausführungs-Threads dürfen jederzeit auf verschiedene Speicherorte gleichzeitig zugreifen (lesen und modifizieren), ohne gegenseitige Beeinflussung und ohne Synchronisierungsanforderungen.
Zwei Ausdrucksauswertungen konfliktieren, wenn eine davon einen Speicherort modifiziert oder die Lebensdauer eines Objekts an einem Speicherort startet/beendet und die andere denselben Speicherort liest oder modifiziert oder die Lebensdauer eines Objekts startet/beendet, das Speicherplatz belegt, der sich mit dem Speicherort überschneidet.
Ein Programm mit zwei konfliktierenden Auswertungen hat einen data race, es sei denn
- beide Auswertungen laufen auf demselben Thread oder in demselben Signal-Handler, oder
- beide konfliktierenden Auswertungen sind atomare Operationen (siehe std::atomic), oder
- eine der konfliktierenden Auswertungen happens-before eine andere (siehe std::memory_order).
Wenn ein data race auftritt, ist das Verhalten des Programms undefiniert.
(Insbesondere ist die Freigabe eines std::mutex synchronisiert mit und daher happens-before die Erfassung desselben Mutex durch einen anderen Thread, was die Verwendung von Mutex-Sperren zum Schutz vor data races ermöglicht.)
int cnt = 0; auto f = [&] { cnt++; }; std::thread t1{f}, t2{f}, t3{f}; // undefined behavior
std::atomic<int> cnt{0}; auto f = [&] { cnt++; }; std::thread t1{f}, t2{f}, t3{f}; // OK
[bearbeiten] Container data races
Alle Container der Standardbibliothek außer std::vector<bool> garantieren, dass gleichzeitige Modifikationen an den Inhalten des enthaltenen Objekts in verschiedenen Elementen desselben Containers niemals zu data races führen.
std::vector<int> vec = {1, 2, 3, 4}; auto f = [&](int index) { vec[index] = 5; }; std::thread t1{f, 0}, t2{f, 1}; // OK std::thread t3{f, 2}, t4{f, 2}; // undefined behavior
std::vector<bool> vec = {false, false}; auto f = [&](int index) { vec[index] = true; }; std::thread t1{f, 0}, t2{f, 1}; // undefined behavior
[bearbeiten] Memory order
Wenn ein Thread einen Wert von einem Speicherort liest, kann er den Anfangswert, den Wert, der im selben Thread geschrieben wurde, oder den Wert, der in einem anderen Thread geschrieben wurde, sehen. Siehe std::memory_order für Details zur Reihenfolge, in der von Threads vorgenommene Schreibvorgänge für andere Threads sichtbar werden.
[bearbeiten] Forward progress
[bearbeiten] Obstruction freedom
Wenn nur ein Thread, der nicht in einer Standardbibliotheksfunktion blockiert ist, eine atomare Funktion ausführt, die lock-free ist, wird garantiert, dass diese Ausführung abgeschlossen wird (alle lock-freien Operationen der Standardbibliothek sind obstruction-free).
[bearbeiten] Lock freedom
Wenn eine oder mehrere lock-freie atomare Funktionen gleichzeitig laufen, wird garantiert, dass mindestens eine davon abgeschlossen wird (alle lock-freien Operationen der Standardbibliothek sind lock-free – es liegt in der Verantwortung der Implementierung sicherzustellen, dass sie nicht durch andere Threads unendlich lange live-locked werden können, z. B. durch ständiges Stehlen der Cache-Zeile).
[bearbeiten] Progress guarantee
In einem gültigen C++-Programm führt jeder Thread schließlich eines der folgenden Dinge aus:
- Beendet.
- Ruft std::this_thread::yield auf.
- Ruft eine Bibliotheks-I/O-Funktion auf.
- Führt einen Zugriff über ein volatile glvalue aus.
- Führt eine atomare Operation oder eine Synchronisationsoperation aus.
- Führt eine triviale Endlosschleife aus (siehe unten).
Ein Thread gilt als fortschreitend, wenn er einen der oben genannten Ausführungsschritte ausführt, in einer Standardbibliotheksfunktion blockiert oder eine atomare lock-freie Funktion aufruft, die aufgrund eines nicht blockierten gleichzeitigen Threads nicht abgeschlossen wird.
Dies ermöglicht es den Compilern, alle Schleifen zu entfernen, zusammenzuführen und neu anzuordnen, die kein beobachtbares Verhalten aufweisen, ohne beweisen zu müssen, dass sie irgendwann terminieren, da sie davon ausgehen können, dass kein Ausführungs-Thread ewig laufen kann, ohne diese beobachtbaren Verhaltensweisen auszuführen. Für triviale Endlosschleifen, die weder entfernt noch neu angeordnet werden können, wird eine Ausnahme gemacht.
[bearbeiten] Trivial infinite loops
Ein trivial leeres Iterationsstatement ist ein Iterationsstatement, das einer der folgenden Formen entspricht:
while ( condition ) ; |
(1) | ||||||||
while ( condition ) { } |
(2) | ||||||||
do ; while ( condition ) ; |
(3) | ||||||||
do { } while ( condition ) ; |
(4) | ||||||||
for ( init-statement condition (optional) ; ) ; |
(5) | ||||||||
for ( init-statement condition (optional) ; ) { } |
(6) | ||||||||
Der kontrollierende Ausdruck eines trivial leeren Iterationsstatements ist
Eine triviale Endlosschleife ist ein trivial leeres Iterationsstatement, für das der konvertierte kontrollierende Ausdruck ein konstanter Ausdruck ist, wenn er manifestly constant-evaluated wird, und zu true ausgewertet wird.
Der Schleifenkörper einer trivialen Endlosschleife wird durch einen Aufruf der Funktion std::this_thread::yield ersetzt. Es ist implementierungsabhängig, ob diese Ersetzung bei freistehenden Implementierungen stattfindet.
for (;;); // trivial infinite loop, well defined as of P2809 for (;;) { int x; } // undefined behavior
Concurrent forward progressWenn ein Thread eine concurrent forward progress guarantee anbietet, wird er in endlicher Zeit fortschreiten (wie oben definiert), solange er nicht beendet wurde, unabhängig davon, ob andere Threads (falls vorhanden) fortschreiten. Der Standard ermutigt, aber verlangt nicht, dass der Hauptthread und die von std::thread und std::jthread(seit C++20) gestarteten Threads eine concurrent forward progress guarantee anbieten. Parallel forward progressWenn ein Thread eine parallel forward progress guarantee anbietet, ist die Implementierung nicht verpflichtet sicherzustellen, dass der Thread schließlich fortschreitet, wenn er noch keinen Ausführungsschritt (I/O, volatile, atomic oder Synchronisation) ausgeführt hat, aber sobald dieser Thread einen Schritt ausgeführt hat, bietet er concurrent forward progress guarantees (diese Regel beschreibt einen Thread in einem Thread-Pool, der Aufgaben in beliebiger Reihenfolge ausführt). Weakly parallel forward progressWenn ein Thread eine weakly parallel forward progress guarantee anbietet, garantiert er nicht, dass er fortschreitet, unabhängig davon, ob andere Threads fortschreiten oder nicht. Solchen Threads kann immer noch der Fortschritt garantiert werden, indem sie mit Delegation der Forward Progress Garantie blockieren: Wenn ein Thread Die parallelen Algorithmen aus der C++-Standardbibliothek blockieren mit Forward Progress Delegation auf den Abschluss einer nicht spezifizierten Menge von Bibliotheksverwalteten Threads. |
(seit C++17) |
[bearbeiten] Defect reports
Die folgenden Verhaltensändernden Fehlerberichte wurden rückwirkend auf zuvor veröffentlichte C++-Standards angewendet.
| DR | angewendet auf | Verhalten wie veröffentlicht | Korrigiertes Verhalten |
|---|---|---|---|
| CWG 1953 | C++11 | zwei Ausdrucksauswertungen, die Lebensdauern von Objekten mit überlappenden Speicherbereichen starten/beenden konfliktieren nicht |
sie stehen im Konflikt |
| LWG 2200 | C++11 | war unklar, ob die Container-Data-Race- Anforderung nur für Sequenzcontainer gilt |
gilt für alle Container |
| P2809R3 | C++11 | das Verhalten der Ausführung von „trivialen“[1] Endlosschleifen war undefiniert |
definiert „triviale Endlosschleifen“ korrekt und machte das Verhalten wohl-definiert |
- ↑ „Trivial“ bedeutet hier, dass die Ausführung der Endlosschleife niemals Fortschritte macht.