Namespaces
Variants

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

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

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
**Erklärung:** - Der gesamte C++ Code innerhalb der `
` 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)
1) Eine while -Anweisung , deren Schleifenkörper eine leere einfache Anweisung ist.
2) Eine while -Anweisung , deren Schleifenkörper eine leere zusammengesetzte Anweisung ist.
3) Eine do - while -Anweisung deren Schleifenkörper eine leere einfache Anweisung ist.
4) Eine do - while -Anweisung deren Schleifenkörper eine leere zusammengesetzte Anweisung ist.
5) Eine for -Anweisung , deren Schleifenkörper eine leere einfache Anweisung ist, hat keine Iterationsausdruck .
6) Eine for -Anweisung , deren Schleifenkörper eine leere zusammengesetzte Anweisung ist, hat keine Iterationsausdruck .

Der controlling expression einer trivially empty iteration statement ist:

1-4) Bedingung .
5,6) Bedingung falls vorhanden, andernfalls true .

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 Progress

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

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

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

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