Angenommen, eine Klasse verfügt über ein public int counter
Feld, auf das mehrere Threads zugreifen. Dies int
wird nur erhöht oder verringert.
Welcher Ansatz sollte verwendet werden, um dieses Feld zu erhöhen, und warum?
lock(this.locker) this.counter++;
,Interlocked.Increment(ref this.counter);
,- Ändern Sie den Zugriffsmodifikator von
counter
inpublic volatile
.
Jetzt, wo ich entdeckt habe volatile
, habe ich viele lock
Aussagen und die Verwendung von entfernt Interlocked
. Aber gibt es einen Grund, dies nicht zu tun?
Antworten:
Das Schlimmste (funktioniert eigentlich nicht)
Wie andere bereits erwähnt haben, ist dies allein überhaupt nicht sicher. Der Punkt
volatile
ist, dass mehrere Threads, die auf mehreren CPUs ausgeführt werden, Daten zwischenspeichern können und werden und Anweisungen neu anordnen.Wenn dies nicht der
volatile
Fall ist und CPU A einen Wert erhöht, sieht CPU B diesen inkrementierten Wert möglicherweise erst einige Zeit später, was zu Problemen führen kann.Wenn
volatile
dies der Fall ist , wird nur sichergestellt, dass die beiden CPUs gleichzeitig dieselben Daten sehen. Es hindert sie überhaupt nicht daran, ihre Lese- und Schreibvorgänge zu verschachteln, was das Problem ist, das Sie vermeiden möchten.Zweitbester:
Dies ist sicher (vorausgesetzt, Sie erinnern sich an alle
lock
anderen Stellen, auf die Sie zugreifenthis.counter
). Es verhindert, dass andere Threads anderen Code ausführen, der von geschützt wirdlocker
. Die Verwendung von Sperren verhindert auch die oben beschriebenen Probleme bei der Neuordnung mehrerer CPUs, was großartig ist.Das Problem ist, dass das Sperren langsam ist. Wenn Sie das
locker
an einem anderen Ort wiederverwenden, der nicht wirklich verwandt ist, können Sie Ihre anderen Threads ohne Grund blockieren.Beste
Dies ist sicher, da das Lesen, Inkrementieren und Schreiben effektiv in einem Treffer ausgeführt wird, der nicht unterbrochen werden kann. Aus diesem Grund wirkt sich dies nicht auf anderen Code aus, und Sie müssen auch nicht daran denken, an anderer Stelle zu sperren. Es ist auch sehr schnell (wie MSDN sagt, ist dies auf modernen CPUs oft buchstäblich eine einzelne CPU-Anweisung).
Ich bin mir jedoch nicht ganz sicher, ob es darum geht, dass andere CPUs Dinge neu anordnen oder ob Sie auch flüchtig mit dem Inkrement kombinieren müssen.InterlockedNotes:
Fußnote: Wofür flüchtig ist eigentlich gut.
Wie
volatile
nicht diese Art von Multithreading - Probleme zu verhindern, was ist es? Ein gutes Beispiel ist, dass Sie zwei Threads haben, einen, der immer in eine Variable schreibt (z. B.queueLength
), und einen, der immer aus derselben Variablen liest.Wenn
queueLength
es nicht flüchtig ist, kann Thread A fünfmal schreiben, aber Thread B kann diese Schreibvorgänge als verzögert (oder möglicherweise sogar in der falschen Reihenfolge) ansehen.Eine Lösung wäre das Sperren, aber Sie könnten in dieser Situation auch volatile verwenden. Dies würde sicherstellen, dass Thread B immer das aktuellste sieht, was Thread A geschrieben hat. Beachten Sie jedoch, dass diese Logik nur funktioniert, wenn Sie Autoren haben, die nie lesen, und Leser, die nie schreiben, und wenn das, was Sie schreiben, ein atomarer Wert ist. Sobald Sie ein einzelnes Lese-, Änderungs- und Schreibvorgang ausführen, müssen Sie zu Interlocked-Vorgängen wechseln oder eine Sperre verwenden.
quelle
BEARBEITEN: Wie in den Kommentaren erwähnt, verwende ich diese Tage gerne
Interlocked
für Fälle einer einzelnen Variablen, in denen es offensichtlich in Ordnung ist. Wenn es komplizierter wird, werde ich immer noch zum Sperren zurückkehren ...Die Verwendung
volatile
hilft nicht, wenn Sie inkrementieren müssen, da Lesen und Schreiben separate Anweisungen sind. Ein anderer Thread kann den Wert nach dem Lesen, aber vor dem Zurückschreiben ändern.Persönlich sperre ich fast immer nur - es ist einfacher, auf eine Weise richtig zu machen, die offensichtlich richtig ist, als entweder Volatilität oder Interlocked.Increment. Für mich ist sperrenfreies Multithreading etwas für echte Threading-Experten, von denen ich keiner bin. Wenn Joe Duffy und sein Team nette Bibliotheken erstellen, die Dinge parallelisieren, ohne so viel zu sperren wie etwas, das ich bauen würde, ist das fabelhaft, und ich werde es sofort verwenden - aber wenn ich das Threading selbst mache, versuche ich es halte es einfach.
quelle
"
volatile
" ersetzt nichtInterlocked.Increment
! Es wird nur sichergestellt, dass die Variable nicht zwischengespeichert, sondern direkt verwendet wird.Das Inkrementieren einer Variablen erfordert tatsächlich drei Operationen:
Interlocked.Increment
führt alle drei Teile als eine einzige atomare Operation aus.quelle
volatile
eigentlich nicht sicher, dass die Variable nicht zwischengespeichert ist. Es gibt nur Einschränkungen, wie es zwischengespeichert werden kann. Zum Beispiel kann es immer noch im L2-Cache der CPU zwischengespeichert werden, da diese in der Hardware kohärent sind. Es kann immer noch bevorzugt werden. Schreibvorgänge können weiterhin in den Cache gestellt werden und so weiter. (Was ich denke, war das, worauf Zach hinaus wollte.)Sie suchen entweder nach einem gesperrten oder einem ineinandergreifenden Inkrement.
Volatile ist definitiv nicht das, wonach Sie suchen - es weist den Compiler einfach an, die Variable so zu behandeln, als würde sie sich ständig ändern, selbst wenn der aktuelle Codepfad es dem Compiler ermöglicht, das Lesen aus dem Speicher anderweitig zu optimieren.
z.B
Wenn m_Var in einem anderen Thread auf false gesetzt ist, aber nicht als flüchtig deklariert ist, kann der Compiler eine Endlosschleife erstellen (dies bedeutet jedoch nicht, dass dies immer der Fall ist), indem er sie mit einem CPU-Register vergleicht (z. B. EAX, weil dies der Fall war) in was m_Var von Anfang an abgerufen wurde) anstatt einen weiteren Lesevorgang an den Speicherort von m_Var zu senden (dies kann zwischengespeichert werden - wir wissen es nicht und es ist uns egal und das ist der Punkt der Cache-Kohärenz von x86 / x64). Alle früheren Beiträge von anderen, die die Neuordnung von Anweisungen erwähnt haben, zeigen einfach, dass sie x86 / x64-Architekturen nicht verstehen. Flüchtig nichtStellen Sie Lese- / Schreibbarrieren aus, wie in den früheren Beiträgen impliziert: "Es verhindert eine Neuordnung". Dank des MESI-Protokolls ist garantiert, dass das von uns gelesene Ergebnis auf allen CPUs immer gleich ist, unabhängig davon, ob die tatsächlichen Ergebnisse in den physischen Speicher verschoben wurden oder sich einfach im Cache der lokalen CPU befinden. Ich werde nicht zu weit in die Details gehen, aber seien Sie versichert, dass Intel / AMD im Falle eines Fehlers wahrscheinlich einen Prozessorrückruf ausgeben würde! Dies bedeutet auch, dass wir uns nicht um die Ausführung außerhalb der Reihenfolge usw. kümmern müssen. Die Ergebnisse werden garantiert immer in der richtigen Reihenfolge in den Ruhestand versetzt - ansonsten sind wir vollgestopft!
Bei Interlocked Increment muss der Prozessor ausgehen, den Wert von der angegebenen Adresse abrufen, dann inkrementieren und zurückschreiben - und das alles, während er das ausschließliche Eigentum an der gesamten Cache-Zeile (lock xadd) hat, um sicherzustellen, dass keine anderen Prozessoren Änderungen vornehmen können dessen Wert.
Mit volatile erhalten Sie immer noch nur eine Anweisung (vorausgesetzt, die JIT ist effizient, wie sie sollte) - inc dword ptr [m_Var]. Der Prozessor (cpuA) fordert jedoch nicht das ausschließliche Eigentum an der Cache-Zeile an, während er alles tut, was er mit der verriegelten Version getan hat. Wie Sie sich vorstellen können, bedeutet dies, dass andere Prozessoren einen aktualisierten Wert nach dem Lesen durch cpuA zurück in m_Var schreiben können. Anstatt den Wert jetzt zweimal zu erhöhen, erhalten Sie nur einmal.
Hoffe, das klärt das Problem.
Weitere Informationen finden Sie unter "Verstehen der Auswirkungen von Low-Lock-Techniken in Multithread-Apps" - http://msdn.microsoft.com/en-au/magazine/cc163715.aspx
ps Was hat diese sehr späte Antwort ausgelöst? Alle Antworten waren in ihrer Erklärung so offensichtlich falsch (insbesondere die als Antwort gekennzeichnete), dass ich sie nur für alle anderen, die dies lesen, klären musste. zuckt die Achseln
pps Ich gehe davon aus, dass das Ziel x86 / x64 und nicht IA64 ist (es hat ein anderes Speichermodell). Beachten Sie, dass die ECMA-Spezifikationen von Microsoft insofern durcheinander sind, als sie das schwächste Speichermodell anstelle des stärksten angeben (es ist immer besser, das stärkste Speichermodell zu verwenden, damit es plattformübergreifend konsistent ist - andernfalls würde Code auf x86 / 24-7 ausgeführt x64 läuft möglicherweise überhaupt nicht auf IA64, obwohl Intel ein ähnlich starkes Speichermodell für IA64 implementiert hat - Microsoft gab dies selbst zu - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx .
quelle
Verriegelte Funktionen werden nicht gesperrt. Sie sind atomar, was bedeutet, dass sie während des Inkrements ohne die Möglichkeit eines Kontextwechsels abgeschlossen werden können. Es besteht also keine Möglichkeit eines Deadlocks oder Wartens.
Ich würde sagen, dass Sie es immer einer Sperre und einem Inkrement vorziehen sollten.
Volatile ist nützlich, wenn Sie Schreibvorgänge in einem Thread benötigen, um in einem anderen gelesen zu werden, und wenn Sie möchten, dass der Optimierer die Vorgänge für eine Variable nicht neu anordnet (weil in einem anderen Thread Dinge geschehen, von denen der Optimierer nichts weiß). Es ist eine orthogonale Wahl, wie Sie inkrementieren.
Dies ist ein wirklich guter Artikel, wenn Sie mehr über sperrenfreien Code und die richtige Vorgehensweise beim Schreiben erfahren möchten
http://www.ddj.com/hpc-high-performance-computing/210604448
quelle
lock (...) funktioniert, blockiert jedoch möglicherweise einen Thread und kann zu einem Deadlock führen, wenn anderer Code dieselben Sperren auf inkompatible Weise verwendet.
Interlocked. * Ist der richtige Weg, dies zu tun ... viel weniger Overhead, da moderne CPUs dies als Grundelement unterstützen.
flüchtig allein ist nicht korrekt. Ein Thread, der versucht, einen geänderten Wert abzurufen und dann zurückzuschreiben, kann immer noch einen Konflikt mit einem anderen Thread haben, der dasselbe tut.
quelle
Ich habe einige Tests durchgeführt, um zu sehen, wie die Theorie tatsächlich funktioniert: kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html . Mein Test konzentrierte sich mehr auf CompareExchnage, aber das Ergebnis für Increment ist ähnlich. Interlocked ist in einer Umgebung mit mehreren CPUs nicht schneller erforderlich. Hier ist das Testergebnis für Increment auf einem 2 Jahre alten 16-CPU-Server. Beachten Sie jedoch, dass der Test auch das sichere Ablesen nach dem Erhöhen umfasst, was in der realen Welt typisch ist.
quelle
Ich stimme der Antwort von Jon Skeet zu und möchte die folgenden Links für alle hinzufügen, die mehr über "volatile" und Interlocked erfahren möchten:
Atomizität, Volatilität und Unveränderlichkeit sind unterschiedlich, Teil eins - (Eric Lipperts Fabulous Adventures In Coding)
Atomizität, Flüchtigkeit und Unveränderlichkeit sind unterschiedlich, Teil zwei
Atomizität, Flüchtigkeit und Unveränderlichkeit sind unterschiedlich, Teil drei
Sayonara Volatile - (Wayback Machine-Schnappschuss von Joe Duffys Weblog, wie er 2012 erschien)
quelle
Ich mag erwähnt in den anderen Antworten den Unterschied zwischen hinzuzufügen
volatile
,Interlocked
undlock
:Das Schlüsselwort volatile kann auf Felder dieses Typs angewendet werden :
sbyte
,byte
,short
,ushort
,int
,uint
,char
,float
, undbool
.byte
,sbyte
,short
, ushort,int
oderuint
.IntPtr
undUIntPtr
.Andere Typen , einschließlich
double
undlong
, können nicht als "flüchtig" markiert werden, da beim Lesen und Schreiben in Felder dieser Typen nicht garantiert werden kann, dass sie atomar sind. Verwenden Sie dieInterlocked
Klassenmitglieder oder den Zugriff mithilfe derlock
Anweisung , um den Multithread-Zugriff auf diese Feldtypen zu schützen .quelle