Ich habe ein einfaches Multithreading-Programm wie folgt geschrieben:
static bool finished = false;
int func()
{
size_t i = 0;
while (!finished)
++i;
return i;
}
int main()
{
auto result=std::async(std::launch::async, func);
std::this_thread::sleep_for(std::chrono::seconds(1));
finished=true;
std::cout<<"result ="<<result.get();
std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}
Es verhält sich normalerweise im Debug-Modus in Visual Studio oder -O0
in gc c und druckt das Ergebnis nach 1
Sekunden aus. Aber es blieb hängen und druckt nichts im Release- Modus oder -O1 -O2 -O3
.
c++
multithreading
thread-safety
data-race
sz ppeter
quelle
quelle
Antworten:
Zwei Threads, die auf eine nicht atomare, nicht geschützte Variable zugreifen, sind UB. Dies betrifft
finished
. Sie könnten einenfinished
Typstd::atomic<bool>
eingeben, um dies zu beheben.Mein Fix:
Ausgabe:
Live-Demo auf coliru
Jemand könnte denken: 'Es ist ein
bool
- wahrscheinlich ein bisschen. Wie kann das nicht atomar sein? ' (Ich habe es getan, als ich selbst mit Multithreading angefangen habe.)Beachten Sie jedoch, dass das Nicht-Zerreißen nicht das einzige ist, was
std::atomic
was Sie erhalten. Außerdem wird der gleichzeitige Lese- und Schreibzugriff von mehreren Threads genau definiert, sodass der Compiler nicht davon ausgehen kann, dass beim erneuten Lesen der Variablen immer derselbe Wert angezeigt wird.Das
bool
Erstellen eines unbewachten, nicht atomaren Geräts kann zusätzliche Probleme verursachen:atomic<bool>
mitmemory_order_relaxed
Speichern / Laden funktionieren würde, aber wovolatile
jedoch nicht. Verwenden volatil dafür wäre UB, obwohl es in der Praxis auf echten C ++ - Implementierungen funktioniert.)Um dies zu verhindern, muss der Compiler ausdrücklich angewiesen werden, dies nicht zu tun.
Ich bin ein wenig überrascht über die sich entwickelnde Diskussion über die mögliche Beziehung
volatile
zu diesem Thema. Daher möchte ich meine zwei Cent ausgeben:quelle
func()
warf einen Blick darauf und dachte: "Ich könnte das weg optimieren." Der Optimierer kümmert sich überhaupt nicht um Threads und erkennt die Endlosschleife und verwandelt sie glücklich in eine "Weile (wahr)", wenn wir Godbolt betrachten .org / z / Tl44iN können wir das sehen. Wenn es fertig istTrue
, kehrt es zurück. Wenn nicht, geht es in einen bedingungslosen Sprung zurück zu sich selbst (eine Endlosschleife) bei Label.L5
volatile
C ++ 11 zu missbrauchen, da Sie mitatomic<T>
und identisch mit asm werden könnenstd::memory_order_relaxed
. Auf realer Hardware funktioniert dies jedoch: Caches sind kohärent, sodass eine Ladeanweisung einen veralteten Wert nicht mehr lesen kann, sobald sich ein Speicher auf einem anderen Kern zum Cache verpflichtet. (MESI)volatile
ist jedoch immer noch UB. Sie sollten wirklich niemals davon ausgehen, dass UB definitiv und eindeutig sicher ist, nur weil Sie sich nicht vorstellen können, wie es schief gehen könnte und es funktioniert hat, als Sie es ausprobiert haben. Das hat Menschen immer und immer wieder verbrannt.finished
mit einemstd::mutex
Werk schützen (ohnevolatile
oderatomic
). Tatsächlich können Sie alle Atomics durch ein "einfaches" Wert + Mutex-Schema ersetzen. es würde immer noch funktionieren und nur langsamer sein.atomic<T>
darf einen internen Mutex verwenden; Nuratomic_flag
ist garantiert sperrenfrei.Scheffs Antwort beschreibt, wie Sie Ihren Code reparieren können. Ich dachte, ich würde ein paar Informationen darüber hinzufügen, was in diesem Fall tatsächlich passiert.
Ich habe Ihren Code bei Godbolt mit Optimierungsstufe 1 (
-O1
) kompiliert . Ihre Funktion kompiliert wie folgt:Also, was passiert hier? Zuerst haben wir einen Vergleich:
cmp BYTE PTR finished[rip], 0
- Dies prüft, obfinished
falsch ist oder nicht.Wenn es nicht falsch ist (auch bekannt als wahr), sollten wir die Schleife beim ersten Lauf verlassen. Dies erreicht durch
jne .L4
die j UMPS wenn n ot e Qual zu Label.L4
wobei der Wert voni
(0
) für eine spätere Verwendung in einem Register gespeichert wird , und die Funktion kehrt zurück.Wenn es jedoch falsch ist, gehen wir zu
Dies ist ein bedingungsloser Sprung zum Beschriften,
.L5
der zufällig der Sprungbefehl selbst ist.Mit anderen Worten, der Thread wird in eine Endlos-Besetzt-Schleife gebracht.
Warum ist das passiert?
Für den Optimierer liegen Threads außerhalb seines Zuständigkeitsbereichs. Es wird davon ausgegangen, dass andere Threads nicht gleichzeitig Variablen lesen oder schreiben (da dies ein Datenrassen-UB wäre). Sie müssen ihm mitteilen, dass die Zugriffe nicht optimiert werden können. Hier kommt Scheffs Antwort ins Spiel. Ich werde mich nicht die Mühe machen, ihn zu wiederholen.
Da dem Optimierer nicht mitgeteilt wird, dass sich die
finished
Variable möglicherweise während der Ausführung der Funktion ändern kann, sieht er,finished
dass sie von der Funktion selbst nicht geändert wird, und geht davon aus, dass sie konstant ist.Der optimierte Code stellt die beiden Codepfade bereit, die sich aus der Eingabe der Funktion mit einem konstanten Bool-Wert ergeben. Entweder wird die Schleife unendlich ausgeführt, oder die Schleife wird nie ausgeführt.
beim
-O0
Compiler (wie erwartet) wird der Schleifenkörper und Vergleich nicht weg optimiert:Daher funktioniert die Funktion, wenn sie nicht optimiert ist, ist der Mangel an Atomizität hier normalerweise kein Problem, da der Code und der Datentyp einfach sind. Wahrscheinlich ist das Schlimmste, auf das wir hier stoßen könnten, ein Wert, der
i
um eins von dem abweicht, was er sein sollte.Ein komplexeres System mit Datenstrukturen führt mit größerer Wahrscheinlichkeit zu beschädigten Daten oder einer fehlerhaften Ausführung.
quelle
atomic
in Code erfinden können , die diese Variablen nicht schreiben. zBif (cond) foo=1;
kann nicht in asm umgewandelt werden,foo = cond ? 1 : foo;
da dieser load + store (kein atomares RMW) auf einen Schreibvorgang von einem anderen Thread aus treten könnte. Compiler vermieden solche Dinge bereits, weil sie für das Schreiben von Multithread-Programmen nützlich sein wollten, aber C ++ 11 machte es offiziell, dass Compiler keinen Code brechen mussten, in den 2 Threads schreiben,a[1]
unda[2]
Der Vollständigkeit halber in der Lernkurve; Sie sollten die Verwendung globaler Variablen vermeiden. Sie haben gute Arbeit geleistet, indem Sie es statisch gemacht haben, sodass es lokal für die Übersetzungseinheit ist.
Hier ist ein Beispiel:
Lebe auf Zauberstabbox
quelle
finished
alsstatic
innerhalb des Funktionsblocks deklarieren . Es wird immer noch nur einmal initialisiert. Wenn es auf eine Konstante initialisiert wird, ist keine Sperrung erforderlich.finished
könnten auch billigerestd::memory_order_relaxed
Ladungen und Geschäfte nutzen; Es ist keine Bestellung erforderlich. andere Variablen in beiden Threads. Ich bin mir jedoch nicht sicher, ob @ Davislors Vorschlagstatic
Sinn macht. Wenn Sie mehrere Spin-Count-Threads hätten, müssten Sie sie nicht alle mit demselben Flag stoppen. Sie möchten die Initialisierung von so schreibenfinished
, dass sie nur zu einer Initialisierung und nicht zu einem atomaren Speicher kompiliert wird. (Wie bei derfinished = false;
Standard-C ++ 17-Syntax des Initialisierers. Godbolt.org/z/EjoKgq ).