Namensräume
Varianten
Aktionen

Multi-threaded executions and data races (seit C++11)

Von cppreference.com
< cpp‎ | Sprache
 
 
C++ Sprache
Allgemeine Themen
Kontrollfluss
Bedingte Ausführungsaussagen
if
Iterationsanweisungen (Schleifen)
for
Bereichs-for (C++11)
Sprunganweisungen
Funktionen
Funktionsdeklaration
Lambda-Funktionsausdruck
inline-Spezifizierer
Dynamische Ausnahmespezifikationen (bis C++17*)
noexcept-Spezifizierer (C++11)
Ausnahmen
Namensräume
Typen
Spezifizierer
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Speicherdauer-Spezifizierer
Initialisierung
Ausdrücke
Alternative Darstellungen
Literale
Boolesch - Ganzzahl - Gleitkommazahl
Zeichen - String - nullptr (C++11)
Benutzerdefinierte (C++11)
Dienstprogramme
Attribute (C++11)
Typen
typedef-Deklaration
Typalias-Deklaration (C++11)
Umwandlungen
Speicherzuweisung
Klassen
Klassenspezifische Funktionseigenschaften
explicit (C++11)
static

Spezielle Member-Funktionen
Templates
Sonstiges
 
 

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)
1) Ein while-Statement, dessen Schleifenkörper ein leeres einfaches Statement ist.
2) Ein while-Statement, dessen Schleifenkörper ein leeres zusammengesetztes Statement ist.
3) Ein do-while-Statement, dessen Schleifenkörper ein leeres einfaches Statement ist.
4) Ein do-while-Statement, dessen Schleifenkörper ein leeres zusammengesetztes Statement ist.
5) Ein for-Statement, dessen Schleifenkörper ein leeres einfaches Statement ist; das for-Statement hat keine iteration-expression.
6) Ein for-Statement, dessen Schleifenkörper ein leeres zusammengesetztes Statement ist; das for-Statement hat keine iteration-expression.

Der kontrollierende Ausdruck eines trivial leeren Iterationsstatements ist

1-4) condition.
5,6) condition, falls vorhanden, sonst true.

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 progress

Wenn 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 progress

Wenn 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 progress

Wenn 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 P auf diese Weise auf den Abschluss einer Menge von Threads S blockiert, dann bietet mindestens ein Thread in S eine Forward Progress Garantie, die gleich oder stärker ist als die von P. Sobald dieser Thread abgeschlossen ist, wird ein anderer Thread in S auf ähnliche Weise gestärkt. Sobald die Menge leer ist, wird P entblockiert.

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
  1. „Trivial“ bedeutet hier, dass die Ausführung der Endlosschleife niemals Fortschritte macht.