Multi-threaded executions and data races (since C++11)
Ein Ausführungsstrang 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 jeden Funktionsaufruf einschließt, der anschließend durch den Strang ausgeführt wird.
- Wenn ein Thread einen anderen erstellt, wird der anfängliche 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 weiterhin von einem anderen Thread über einen Zeiger oder per Referenz zugegriffen werden.
- Unter einer Hosted-Implementierung kann ein C++-Programm mehr als einen Thread haben, der gleichzeitig läuft. Die Ausführung jedes Threads erfolgt wie durch den Rest dieser Seite definiert. Die Ausführung des gesamten Programms besteht aus einer Ausführung aller seiner Threads.
- Unter einer Freestanding-Implementierung ist implementierungsdefiniert, 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 den Signal-Handler-Aufruf enthält.
Inhaltsverzeichnis |
Data Races
Unterschiedliche Ausführungsstränge dürfen stets gleichzeitig auf verschiedene Speicherbereiche zugreifen (lesen und ändern), ohne Interferenz und ohne Synchronisierungsanforderungen.
Zwei Ausdrucks Auswertungen konfligieren wenn eine davon einen Speicherbereich modifiziert oder die Lebensdauer eines Objekts in einem Speicherbereich beginnt/beendet, und die andere denselben Speicherbereich liest oder modifiziert oder die Lebensdauer eines Objekts beginnt/beendet, das Speicher belegt, der mit dem Speicherbereich überlappt.
Ein Programm, das zwei konfligierende Auswertungen hat, hat einen data race es sei denn
- beide Auswertungen werden im selben Thread oder im selben Signal-Handler ausgeführt, oder
- beide konfligierenden Auswertungen sind atomare Operationen (siehe std::atomic ), oder
- eine der konfligierenden Auswertungen happens-before einer anderen (siehe std::memory_order ).
Wenn ein Datenwettlauf auftritt, ist das Verhalten des Programms undefiniert.
(Insbesondere ist die Freigabe eines std::mutex synchronized-with , und daher happens-before dem Erwerb desselben Mutex durch einen anderen Thread, was es ermöglicht, Mutex-Sperren zum Schutz vor Datenrennen zu verwenden.)
int cnt = 0; auto f = [&] { cnt++; }; std::thread t1{f}, t2{f}, t3{f}; // undefiniertes Verhalten
std::atomic<int> cnt{0}; auto f = [&] { cnt++; }; std::thread t1{f}, t2{f}, t3{f}; // OK
` und `` Tags bleibt unverändert, wie angefordert
- HTML-Tags und Attribute wurden nicht übersetzt
- C++-spezifische Begriffe wie `std::atomic`, `std::thread`, `auto`, etc. bleiben auf Englisch
- Nur der Kommentar `// OK` könnte theoretisch übersetzt werden, wurde aber hier beibehalten, da er Teil des Code-Beispiels ist
- Die Formatierung und Struktur der HTML-Elemente bleibt vollständig erhalten
Container-Datenrennen
Alle
Container
in der Standardbibliothek außer
std
::
vector
<
bool
>
garantieren, dass gleichzeitige Modifikationen an Inhalten des enthaltenen Objekts in verschiedenen Elementen desselben Containers niemals zu Datenrennen 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}; // undefiniertes Verhalten
std::vector<bool> vec = {false, false}; auto f = [&](int index) { vec[index] = true; }; std::thread t1{f, 0}, t2{f, 1}; // undefiniertes Verhalten
Speicherreihenfolge
Wenn ein Thread einen Wert von einer Speicheradresse liest, kann er den Anfangswert, den in demselben Thread geschriebenen Wert oder den in einem anderen Thread geschriebenen Wert sehen. Siehe std::memory_order für Details zur Reihenfolge, in der Schreibvorgänge aus Threads für andere Threads sichtbar werden.
Fortschrittsgarantie
Hindernisfreiheit
Wenn nur ein Thread, der nicht in einer Standardbibliotheksfunktion blockiert ist, eine atomare Funktion ausführt, die lock-free ist, ist garantiert, dass diese Ausführung abgeschlossen wird (alle lock-free Operationen der Standardbibliothek sind obstruction-free ).
Lock-Freiheit
Wenn eine oder mehrere sperrenfreie atomare Funktionen gleichzeitig ausgeführt werden, ist garantiert, dass mindestens eine von ihnen abgeschlossen wird (alle sperrenfreien Operationen der Standardbibliothek sind lock-free — es ist die Aufgabe der Implementierung sicherzustellen, dass sie nicht durch andere Threads unbegrenzt live-locked werden können, beispielsweise durch kontinuierliches Stehlen der Cache-Zeile).
Fortschrittsgarantie
In einem gültigen C++-Programm führt jeder Thread schließlich eines der folgenden Dinge aus:
- Beendet.
- Ruft std::this_thread::yield auf.
- Führt einen Aufruf einer Bibliotheks-E/A-Funktion durch.
- Führt einen Zugriff über einen volatile Glvalue durch.
- Führt eine atomare Operation oder eine Synchronisationsoperation durch.
- Setzt die Ausführung einer trivialen Endlosschleife fort (siehe unten).
Ein Thread wird als Fortschritt machend bezeichnet, wenn er einen der oben genannten Ausführungsschritte durchführt, in einer Standardbibliotheksfunktion blockiert oder eine atomare lockfreie Funktion aufruft, die aufgrund eines nicht blockierten gleichzeitigen Threads nicht abgeschlossen wird.
Dies ermöglicht es den Compilern, alle Schleifen zu entfernen, zu verschmelzen und neu anzuordnen, die kein beobachtbares Verhalten aufweisen, ohne beweisen zu müssen, dass sie irgendwann terminieren würden, da angenommen werden kann, dass kein Ausführungsstrang ewig laufen kann, ohne eines dieser beobachtbaren Verhalten auszuführen. Eine Ausnahme wird für triviale Endlosschleifen gemacht, die weder entfernt noch neu angeordnet werden können.
Triviale Endlosschleifen
Eine trivial leere Iterationsanweisung ist eine Iterationsanweisung, die einer der folgenden Formen entspricht:
while (
Bedingung
) ;
|
(1) | ||||||||
while (
Bedingung
) { }
|
(2) | ||||||||
do ; while (
Bedingung
) ;
|
(3) | ||||||||
do { } while (
Bedingung
) ;
|
(4) | ||||||||
for (
Initialisierungsanweisung Bedingung
(optional)
; ) ;
|
(5) | ||||||||
for (
Initialisierungsanweisung Bedingung
(optional)
; ) { }
|
(6) | ||||||||
Der controlling expression einer trivially empty iteration statement ist:
Eine triviale Endlosschleife ist eine trivial leere Iterationsanweisung, für die der konvertierte Steuerausdruck ein konstanter Ausdruck ist, wenn offensichtlich konstant ausgewertet , und zu true ausgewertet wird.
Der Schleifenkörper einer trivialen Endlosschleife wird durch einen Aufruf der Funktion std::this_thread::yield ersetzt. Es ist implementierungsdefiniert, ob dieser Ersatz auf freestanding implementations erfolgt.
for (;;); // trivialer Endlosschleife, wohldefiniert ab P2809 for (;;) { int x; } // undefiniertes Verhalten
Concurrent Forward ProgressWenn ein Thread eine Concurrent Forward Progress-Garantie bietet, wird er Fortschritt machen (wie oben definiert) in einer endlichen Zeitspanne, solange er nicht beendet wurde, unabhängig davon, ob andere Threads (falls vorhanden) Fortschritt machen. Der Standard ermutigt, erfordert aber nicht, dass der Hauptthread und die durch std::thread und std::jthread (seit C++20) gestarteten Threads eine Concurrent Forward Progress-Garantie bieten. Parallel Forward ProgressWenn ein Thread eine Parallel Forward Progress-Garantie bietet, ist die Implementierung nicht verpflichtet sicherzustellen, dass der Thread letztendlich Fortschritt macht, falls er noch keinen Ausführungsschritt (I/O, volatile, atomar oder Synchronisation) ausgeführt hat. Sobald dieser Thread jedoch einen Schritt ausgeführt hat, bietet er Concurrent Forward Progress -Garantien (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-Garantie bietet, garantiert er nicht, letztendlich Fortschritt zu machen, unabhängig davon, ob andere Threads Fortschritt machen oder nicht.
Solchen Threads kann dennoch Fortschritt garantiert werden, indem sie mit Forward Progress-Garantiedelegierung blockieren: Wenn ein Thread
Die parallelen Algorithmen der C++-Standardbibliothek blockieren mit Forward Progress-Delegierung auf den Abschluss einer nicht näher spezifizierten Menge von bibliotheksverwalteten Threads. |
(seit C++17) |
Fehlerberichte
Die folgenden verhaltensändernden Fehlerberichte wurden rückwirkend auf zuvor veröffentlichte C++-Standards angewendet.
| DR | Angewendet auf | Verhalten wie veröffentlicht | Korrektes Verhalten |
|---|---|---|---|
| CWG 1953 | C++11 |
zwei Ausdrucksauswertungen, die Lebensdauern von Objekten
mit überlappenden Speicherbereichen beginnen/beenden, erzeugten keinen Konflikt |
sie erzeugen einen Konflikt |
| LWG 2200 | C++11 |
es war unklar, ob die Container-Datenwettlauf-Anforderung
nur für Sequenzcontainer gilt |
gilt für alle Container |
| P2809R3 | C++11 |
das Verhalten bei der Ausführung von "trivialen"
[1]
Endlosschleifen war undefiniert |
definiert "triviale Endlosschleifen" ordnungsgemäß
und machte das Verhalten wohldefiniert |
- ↑ "Trivial" bedeutet hier, dass die Ausführung der Endlosschleife niemals Fortschritte erzielt.