Ich las eine Antwort durch , die Jon Skeet auf eine Frage gab, und darin erwähnte er Folgendes:
Für mich ist sperrenfreies Multithreading etwas für echte Threading-Experten, von denen ich keiner bin.
Es ist nicht das erste Mal, dass ich das höre, aber ich finde nur sehr wenige Leute, die darüber sprechen, wie Sie es tatsächlich tun, wenn Sie lernen möchten, wie man sperrfreien Multithreading-Code schreibt.
Meine Frage ist also, dass Sie nicht nur alles über Threading usw. lernen, sondern auch lernen, wie man spezifisch sperrfreien Multithreading-Code schreibt und welche guten Ressourcen es gibt.
Prost
c#
.net
multithreading
lock-free
vdhant
quelle
quelle
Antworten:
Aktuelle "sperrenfreie" Implementierungen folgen die meiste Zeit demselben Muster:
(* 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).
quelle
Joe Duffys Buch:
http://www.bluebytesoftware.com/books/winconc/winconc_book_resources.html
Er schreibt auch einen Blog zu diesen Themen.
Der Trick, um Low-Lock-Programme richtig zu machen, besteht darin, auf einer tiefen Ebene genau zu verstehen , welche Regeln das Speichermodell für Ihre spezielle Kombination aus Hardware, Betriebssystem und Laufzeitumgebung enthält.
Ich persönlich bin nicht annähernd klug genug, um über InterlockedIncrement hinaus eine korrekte Low-Lock-Programmierung durchzuführen, aber wenn Sie großartig sind, machen Sie es. Stellen Sie einfach sicher, dass Sie viel Dokumentation im Code belassen, damit Personen, die nicht so schlau sind wie Sie, nicht versehentlich eine Ihrer Speichermodellinvarianten brechen und einen nicht zu findenden Fehler einführen.
quelle
Heutzutage gibt es kein "sperrenfreies Einfädeln". Es war ein interessanter Spielplatz für Akademiker und dergleichen, Ende des letzten Jahrhunderts, als Computerhardware langsam und teuer war. Dekkers Algorithmus war immer mein Favorit, moderne Hardware hat ihn auf die Weide gestellt. Es funktioniert nicht mehr.
Zwei Entwicklungen haben dies beendet: die wachsende Ungleichheit zwischen der Geschwindigkeit von RAM und CPU. Und die Fähigkeit der Chiphersteller, mehr als einen CPU-Kern auf einen Chip zu setzen.
Aufgrund des RAM-Geschwindigkeitsproblems mussten die Chipdesigner einen Puffer auf den CPU-Chip setzen. Der Puffer speichert Code und Daten, auf die der CPU-Kern schnell zugreifen kann. Und kann viel langsamer vom / in den RAM gelesen und geschrieben werden. Dieser Puffer wird als CPU-Cache bezeichnet, die meisten CPUs haben mindestens zwei davon. Der Cache der ersten Ebene ist klein und schnell, der zweite ist groß und langsamer. Solange die CPU Daten und Anweisungen aus dem Cache der ersten Ebene lesen kann, läuft sie schnell. Ein Cache-Fehler ist sehr teuer. Er versetzt die CPU in einen Ruhezustand von bis zu 10 Zyklen, wenn sich die Daten nicht im 1. Cache befinden, und in 200 Zyklen, wenn sie sich nicht im 2. Cache befinden und aus dem sie gelesen werden müssen RAM.
Jeder CPU-Kern hat seinen eigenen Cache, sie speichern ihre eigene "Ansicht" des RAM. Wenn die CPU Daten schreibt, wird der Schreibvorgang in den Cache ausgeführt, der dann langsam in den RAM geleert wird. Es ist unvermeidlich, dass jeder Kern jetzt eine andere Ansicht des RAM-Inhalts hat. Mit anderen Worten, eine CPU weiß nicht, was eine andere CPU geschrieben hat, bis dieser RAM-Schreibzyklus abgeschlossen ist und die CPU ihre eigene Ansicht aktualisiert.
Das ist dramatisch inkompatibel mit Threading. Es ist Ihnen immer sehr wichtig, wie der Status eines anderen Threads ist, wenn Sie Daten lesen müssen, die von einem anderen Thread geschrieben wurden. Um dies sicherzustellen, müssen Sie explizit eine sogenannte Speicherbarriere programmieren. Es handelt sich um ein CPU-Grundelement auf niedriger Ebene, das sicherstellt, dass sich alle CPU-Caches in einem konsistenten Zustand befinden und über eine aktuelle Ansicht des Arbeitsspeichers verfügen. Alle ausstehenden Schreibvorgänge müssen in den Arbeitsspeicher geleert werden. Die Caches müssen dann aktualisiert werden.
Dies ist in .NET verfügbar. Die Thread.MemoryBarrier () -Methode implementiert eine. Angesichts der Tatsache, dass dies 90% der Arbeit ist, die die Lock-Anweisung ausführt (und 95 +% der Ausführungszeit), sind Sie einfach nicht voraus, indem Sie die von .NET bereitgestellten Tools meiden und versuchen, Ihre eigenen zu implementieren.
quelle
atomic
Block platzieren können. Alles in allem kann es in vielen Fällen genauso schwierig sein, schlossfreie Strukturen zu konsumieren.Google für sperrenfreie Datenstrukturen und Software-Transaktionsspeicher .
Ich werde John Skeet in diesem Punkt zustimmen. Lock-Free-Threading ist der Spielplatz des Teufels und am besten Menschen überlassen, die wissen, dass sie wissen, was sie wissen müssen.
quelle
Wenn es um Multithreading geht, muss man genau wissen, was man tut. Ich meine, untersuchen Sie alle möglichen Szenarien / Fälle, die auftreten können, wenn Sie in einer Multithread-Umgebung arbeiten. Lock-free Multithreading ist keine Bibliothek oder Klasse, die wir integrieren, sondern ein Wissen / eine Erfahrung, die wir auf unserer Reise mit Threads sammeln.
quelle
Auch wenn sperrfreies Threading in .NET schwierig sein kann, können Sie bei der Verwendung einer Sperre häufig erhebliche Verbesserungen erzielen, indem Sie genau untersuchen, was gesperrt werden muss, und den gesperrten Abschnitt minimieren. Dies wird auch als Minimierung der Granularität der Sperre bezeichnet .
Angenommen, Sie müssen einen Sammlungsthread sicher machen. Werfen Sie nicht einfach blind eine Sperre um eine Methode, die über die Sammlung iteriert, wenn sie für jedes Element eine CPU-intensive Aufgabe ausführt. Möglicherweise müssen Sie nur eine Sperre setzen, um eine flache Kopie der Sammlung zu erstellen. Das Durchlaufen der Kopie könnte dann ohne Sperre funktionieren. Natürlich hängt dies stark von den Besonderheiten Ihres Codes ab, aber ich konnte mit diesem Ansatz ein Problem mit dem Sperrkonvoi beheben .
quelle