Aktuelle "sperrenfreie" Implementierungen folgen die meiste Zeit demselben Muster:
- * Lies einen Zustand und mache eine Kopie davon **
- * Kopie ändern **
- Führen Sie eine verriegelte Operation durch
- Wiederholen Sie den Vorgang, wenn dies fehlschlägt
(* optional: abhängig von der Datenstruktur / dem Algorithmus)
Das letzte Bit ähnelt unheimlich einem Spinlock. In der Tat ist es ein grundlegender Spinlock . :)
Ich stimme @nobugz darin zu: Die Kosten für die ineinandergreifenden Operationen, die beim sperrenfreien Multithreading verwendet werden , werden von den Cache- und Speicherkohärenzaufgaben dominiert, die sie ausführen müssen .
Was Sie jedoch mit einer Datenstruktur gewinnen, die "sperrenfrei" ist, ist, dass Ihre "Sperren" sehr feinkörnig sind . Dies verringert die Wahrscheinlichkeit, dass zwei gleichzeitige Threads auf dieselbe "Sperre" (Speicherort) zugreifen.
Der Trick besteht meistens darin, dass Sie keine dedizierten Sperren haben. Stattdessen behandeln Sie z. B. alle Elemente in einem Array oder alle Knoten in einer verknüpften Liste als "Spin-Lock". Sie lesen, ändern und versuchen zu aktualisieren, wenn seit dem letzten Lesen keine Aktualisierung stattgefunden hat. Wenn ja, versuchen Sie es erneut.
Dies macht Ihr "Sperren" (oh, sorry, nicht sperren :) sehr feinkörnig, ohne zusätzlichen Speicher- oder Ressourcenbedarf einzuführen.
Wenn Sie es feinkörniger machen, verringert sich die Wahrscheinlichkeit von Wartezeiten. Es klingt großartig, es so feinkörnig wie möglich zu gestalten, ohne zusätzliche Ressourcenanforderungen einzuführen, nicht wahr?
Der größte Spaß kann jedoch durch die Sicherstellung der korrekten Bestellung von Laden / Laden entstehen .
Entgegen der eigenen Intuition können CPUs Speicher-Lese- / Schreibvorgänge neu anordnen - sie sind übrigens sehr intelligent: Es wird Ihnen schwer fallen, dies von einem einzigen Thread aus zu beobachten. Sie werden jedoch auf Probleme stoßen, wenn Sie mit dem Multithreading auf mehreren Kernen beginnen. Ihre Intuitionen werden zusammenbrechen: Nur weil eine Anweisung früher in Ihrem Code ist, bedeutet dies nicht, dass sie tatsächlich früher ausgeführt wird. CPUs können Anweisungen in unregelmäßiger Reihenfolge verarbeiten. Dies gilt insbesondere für Anweisungen mit Speicherzugriffen, um die Hauptspeicherlatenz zu verbergen und ihren Cache besser zu nutzen.
Nun ist es gegen die Intuition sicher, dass eine Codesequenz nicht "von oben nach unten" fließt, sondern so läuft, als ob es überhaupt keine Sequenz gäbe - und möglicherweise als "Spielplatz des Teufels" bezeichnet werden kann. Ich glaube, es ist unmöglich, eine genaue Antwort darauf zu geben, welche Nachbestellungen beim Laden / Speichern stattfinden werden. Stattdessen spricht man immer in Bezug auf May und mights und Dosen und auf das Schlimmste vorzubereiten. "Oh, die CPU könnte diesen Lesevorgang so anordnen, dass er vor diesem Schreibvorgang erfolgt. Daher ist es am besten, hier an dieser Stelle eine Speicherbarriere anzubringen."
Angelegenheiten werden durch die Tatsache kompliziert , dass selbst dieses May und mights über CPU - Architekturen unterscheiden können. Es kann beispielsweise der Fall sein, dass etwas, das in einer Architektur garantiert nicht passiert, auf einer anderen Architektur passiert .
Um "sperrfreies" Multithreading richtig zu machen, müssen Sie Speichermodelle verstehen.
Das Speichermodell und die Garantien korrekt zu machen, ist jedoch nicht trivial, wie diese Geschichte zeigt, in der Intel und AMD einige Korrekturen an der Dokumentation vorgenommen haben, die MFENCE
bei JVM-Entwicklern für Aufsehen gesorgt hat . Wie sich herausstellte, war die Dokumentation, auf die sich die Entwickler von Anfang an stützten, überhaupt nicht so präzise.
Sperren in .NET führen zu einer impliziten Speicherbarriere, sodass Sie sie sicher verwenden können (meistens ... siehe zum Beispiel die Größe von Joe Duffy - Brad Abrams - Vance Morrison zu verzögerter Initialisierung, Sperren, flüchtigen Bestandteilen und Speicher Barrieren. :) (Folgen Sie unbedingt den Links auf dieser Seite.)
Als zusätzlichen Bonus werden Sie auf einer Nebenquest in das .NET-Speichermodell eingeführt . :) :)
Es gibt auch einen "Oldie but Goldie" von Vance Morrison: Was jeder Entwickler über Multithread-Apps wissen muss .
... und natürlich ist Joe Duffy , wie @Eric erwähnte, eine definitive Lektüre zu diesem Thema.
Ein gutes STM kann einer feinkörnigen Verriegelung so nahe wie möglich kommen und bietet wahrscheinlich eine Leistung, die einer handgefertigten Implementierung nahe kommt oder dieser ebenbürtig ist. Eines davon ist STM.NET aus den DevLabs-Projekten von MS.
Wenn Sie kein reiner .NET-Fanatiker sind, hat Doug Lea in JSR-166 großartige Arbeit geleistet .
Cliff Click hat eine interessante Sicht auf Hash-Tabellen, die nicht auf Lock-Striping basiert - wie es die gleichzeitigen Hash-Tabellen von Java und .NET tun - und scheint gut auf 750 CPUs zu skalieren.
Wenn Sie keine Angst haben, sich in das Gebiet von Linux zu wagen, bietet der folgende Artikel weitere Einblicke in die Interna aktueller Speicherarchitekturen und wie die gemeinsame Nutzung von Cache-Zeilen die Leistung beeinträchtigen kann: Was jeder Programmierer über Speicher wissen sollte .
@ Ben machte viele Kommentare zu MPI: Ich stimme aufrichtig zu, dass MPI in einigen Bereichen glänzen kann. Eine MPI-basierte Lösung kann einfacher zu überlegen, einfacher zu implementieren und weniger fehleranfällig sein als eine halbherzige Sperrimplementierung, die versucht, intelligent zu sein. (Subjektiv gilt dies jedoch auch für eine STM-basierte Lösung.) Ich würde auch wetten, dass es Lichtjahre einfacher ist, eine anständige verteilte Anwendung in z. B. Erlang korrekt zu schreiben , wie viele erfolgreiche Beispiele nahe legen.
MPI hat jedoch seine eigenen Kosten und seine eigenen Probleme, wenn es auf einem einzelnen Multi-Core-System ausgeführt wird . In Erlang müssen beispielsweise Probleme bei der Synchronisierung von Prozessplanung und Nachrichtenwarteschlangen gelöst werden .
Außerdem implementieren MPI-Systeme im Kern normalerweise eine Art kooperative N: M-Planung für "Lightweight-Prozesse". Dies bedeutet zum Beispiel, dass es einen unvermeidlichen Kontextwechsel zwischen einfachen Prozessen gibt. Es ist wahr, dass es sich nicht um einen "klassischen Kontextwechsel" handelt, sondern hauptsächlich um eine User-Space-Operation, die schnell durchgeführt werden kann. Ich bezweifle jedoch aufrichtig, dass sie unter die 20-200 Zyklen einer ineinandergreifenden Operation gebracht werden kann . Die Kontextumschaltung im Benutzermodus ist sicherlich langsamersogar in der Intel McRT-Bibliothek. N: M-Planung mit leichten Prozessen ist nicht neu. LWPs waren lange Zeit in Solaris vorhanden. Sie wurden verlassen. Es gab Fasern in NT. Sie sind jetzt meistens ein Relikt. Es gab "Aktivierungen" in NetBSD. Sie wurden verlassen. Linux hatte seine eigene Sicht auf das Thema N: M-Threading. Es scheint inzwischen etwas tot zu sein.
Von Zeit zu Zeit gibt es neue Konkurrenten: zum Beispiel McRT von Intel oder zuletzt User-Mode Scheduling zusammen mit ConCRT von Microsoft.
Auf der untersten Ebene machen sie das, was ein N: M MPI-Scheduler macht. Erlang - oder ein beliebiges MPI-System - kann auf SMP-Systemen durch die Nutzung des neuen UMS erheblich profitieren .
Ich denke, die Frage des OP bezieht sich nicht auf die Vorzüge und subjektiven Argumente für / gegen eine Lösung, aber wenn ich das beantworten müsste, hängt es wohl von der Aufgabe ab: für den Aufbau von Basisdatenstrukturen mit niedrigem Niveau und hoher Leistung, die auf a laufen Ein einzelnes System mit vielen Kernen , entweder Low-Lock- / "Lock-Free" -Techniken oder ein STM, liefert die besten Ergebnisse in Bezug auf die Leistung und würde wahrscheinlich eine MPI-Lösung jederzeit in Bezug auf die Leistung schlagen, selbst wenn die oben genannten Falten ausgebügelt werden zB in Erlang.
Um etwas mäßig komplexeres zu erstellen, das auf einem einzelnen System ausgeführt wird, würde ich vielleicht die klassische grobkörnige Verriegelung oder, wenn die Leistung von großer Bedeutung ist, ein STM wählen.
Für den Aufbau eines verteilten Systems würde ein MPI-System wahrscheinlich eine natürliche Wahl treffen.
Beachten Sie, dass es auch MPI-Implementierungen für .NET gibt (obwohl sie nicht so aktiv zu sein scheinen).