Da m
ist eine Variable vom Typ std::mutex
:
diese Sequenz Stellen Sie sich vor:
int a;
m.lock();
b += 1;
a = b;
m.unlock();
do_something_with(a);
Es gibt ein 'offensichtliches', was hier los ist:
Die Zuordnung eines von b und das Inkrement von b wird vor Interferenz von anderen Threads "geschützt", weil andere Threads versuchen, dasselbe zu sperren m
und blockiert werden, bis wir m.unlock()
aufrufen.
Und es ist eine subtile Sache los.
In Singlethread-Code wird der Compiler versuchen, Lasten und speichert neu zu ordnen. Ohne die Schlösser, würde der Compiler frei sein, um effektiv den Code neu zu schreiben, wenn dies effizienter auf dem Chipsatz entpuppt:
int a = b + 1;
// m.lock();
b = a;
// m.unlock();
do_something_with(a);
Oder auch:
do_something_with(++b);
jedoch std::mutex::lock()
, unlock()
std::thread()
, std::async()
, std::future::get()
und so weiter sind Zäune. Der Compiler "weiß", dass er Lasten nicht neu ordnen und speichern (lesen und schreiben) darf, sodass die Operation auf der anderen Seite des Zauns endet, an der Sie beim Schreiben des Codes angegeben haben.
1:
2: m.lock(); <--- This is a fence
3: b += 1; <--- So this load/store operation may not move above line 2
4: m.unlock(); <-- Nor may it be moved below this line
Stellen Sie sich vor, was passieren würde, wenn dies nicht der Fall war:
(Neu geordnete Code)
thread1: int a = b + 1;
<--- Here another thread precedes us and executes the same block of code
thread2: int a = b + 1;
thread2: m.lock();
thread2: b = a;
thread2: m.unlock();
thread1: m.lock();
thread1: b = a;
thread1: m.unlock();
thread1:do_something_with(a);
thread2:do_something_with(a);
Wenn Sie es durch folgen, werden Sie sehen, dass b hat jetzt die falsche Wert, weil der Compiler den Code schneller gemacht hat.
... und das sind nur die Compileroptimierungen. std::mutex
usw. verhindert auch, dass die Speichercaches Lasten neu anordnen und auf eine "optimalere" Weise speichern, was in einer Single-Thread-Umgebung gut wäre, in einem System mit mehreren Kernen (d. H. Jedem modernen PC oder Telefon) jedoch katastrophal. Die Kosten für Thread A müssen geleert werden, bevor Thread B die gleichen Daten liest, und das Leeren von Caches in den Arbeitsspeicher ist im Vergleich zum Cache-Speicherzugriff schrecklich langsam.
Es gibt Kosten für diese Sicherheit. Aber c'est la vie. Nur so kann die gleichzeitige Ausführung sichergestellt werden.
Deshalb bevorzugen wir, dass, wenn möglich, in einem SMP-System jeder Thread seine eigene Kopie von Daten hat, auf der er arbeiten kann. Wir möchten nicht nur die Zeit in einer Schleuse minimieren, sondern auch die Anzahl der Überfahrten.
Ich könnte weiter über die Modifikatoren std::memory_order
sprechen, aber das ist ein dunkles und gefährliches Loch, das Experten oft falsch verstehen und in denen Anfänger keine Hoffnung haben, es richtig zu machen.
Der Mutex sperrt keine Variablen, er blockiert lediglich einen Thread, der versucht, ihn zu erhalten.Dies hat den Effekt, Variablen zu schützen, wenn das Programm nur zwischen einem 'lock()' - und einem 'unlock() '-Aufruf auf sie zugreift. Das Vorhandensein des Mutex 'lock()' und 'unlock()' im Code verhindert auch, dass der Compiler den Speicherzugriff der so geschützten * accesses * neu anordnet. –
Sie müssen auch sicherstellen, dass die Threads das gleiche Mutex-Objekt verwenden. Einen unterschiedlichen Mutex in jedem Thread zu haben, wird nichts tun. Stattdessen blockiert es, wenn ein anderer Thread versucht, einen bereits gesperrten Mutex wie @RichardHodges zu sperren. – Hayt
Danke, das hat es wirklich geklärt! – niko