Dies gilt nicht nur für Java, sondern generell für die Thread-Programmierung. Ich merke, dass ich die meisten Nebenläufigkeits- und Latenzprobleme einfach dadurch vermeide, dass ich folgende Richtlinien befolge:
1 / Lassen Sie jeden Thread seine eigene Lebensdauer (d. h. entscheiden, wann zu sterben). Es kann von außen (sagen wir eine Flag-Variable), aber es ist völlig verantwortlich.
2 / Haben alle -Threads ihre Ressourcen in der gleichen Reihenfolge zugewiesen und freigegeben - dies garantiert, dass kein Deadlock auftritt.
3 / Ressourcen für die kürzest mögliche Zeit sperren.
4 / Übernehmen Sie die Verantwortung für Daten mit den Daten selbst. Sobald Sie einem Thread mitgeteilt haben, dass die Daten verarbeitet werden sollen, lassen Sie es in Ruhe , bis die Verantwortung Ihnen zurückgegeben wird.
Ich folge typischerweise einem Ansatz nach Erlang. Ich benutze das aktive Objektmuster. Es funktioniert wie folgt.
Teilen Sie Ihre Anwendung in sehr grobkörnige Einheiten ein. In einer meiner aktuellen Anwendungen (400.000 LOC) habe ich ca. 8 dieser grobkörnigen Einheiten. Diese Einheiten teilen überhaupt keine Daten. Jede Einheit behält ihre eigenen lokalen Daten. Jede Unit läuft auf einem eigenen Thread (= Active Object Pattern) und ist daher single threaded. Sie brauchen keine Schlösser in den Einheiten. Wenn die Einheiten Nachrichten an andere Einheiten senden müssen, tun sie dies, indem sie eine Nachricht an eine Warteschlange der anderen Einheiten senden. Die andere Einheit wählt die Nachricht aus der Warteschlange aus und reagiert auf diese Nachricht. Dies könnte andere Nachrichten an andere Einheiten auslösen. Folglich sind die einzigen Sperren in diesem Anwendungstyp die Warteschlangen (eine Warteschlange und eine Sperre pro Einheit). Diese Architektur ist definitionsgemäß Deadlock-frei!
Diese Architektur ist extrem skalierbar und lässt sich sehr einfach implementieren und erweitern, sobald Sie das Grundprinzip verstanden haben. Es sieht es gerne als eine SOA innerhalb einer Anwendung.
Indem du deine App in die Einheiten aufteilst, erinnere dich daran. Die optimale Anzahl von langen laufenden Threads pro CPU-Kern ist 1.
Es gibt keine Eine wahre Antwort für die Thread-Sicherheit in Java. Es gibt jedoch mindestens ein wirklich tolles Buch: Java Concurrency in Practice . Ich beziehe mich regelmäßig darauf (vor allem die Online-Safari-Version, wenn ich auf Reisen bin).
Ich empfehle dringend, dieses Buch eingehend zu lesen. Sie werden feststellen, dass Kosten und Nutzen Ihres unkonventionellen Ansatzes gründlich untersucht werden.
Es gibt eine Reihe von Techniken, die gerade jetzt in das öffentliche Bewusstsein kommen (wie in den letzten Jahren). Ein großer wäre Schauspieler. Das hat Erlang zuerst ins Netz gebracht, aber von neueren Sprachen wie Scala (Schauspieler an der JVM) übernommen. Es stimmt zwar, dass die Akteure nicht jedes Problem lösen, sie machen es jedoch viel einfacher, über Ihren Code nachzudenken und Problemstellen zu identifizieren. Sie machen es auch viel einfacher, parallele Algorithmen zu entwerfen, weil sie Sie zwingen, die Fortsetzung zu verwenden, die über den gemeinsamen veränderlichen Zustand geht.
Fork / Join sollten Sie sich ansehen, besonders wenn Sie auf der JVM sind. Doug Lea hat das bahnbrechende Papier zum Thema geschrieben, aber viele Forscher haben es im Laufe der Jahre diskutiert. Wie ich es verstehe, ist das Referenz-Framework von Doug Lea für die Integration in Java 7 geplant.
Auf einer etwas weniger invasiven Ebene sind häufig die einzigen Schritte, die zur Vereinfachung einer Multithread-Anwendung erforderlich sind, lediglich die Verringerung der Komplexität der Verriegelung. Feingranulares Sperren (im Java 5-Stil) ist großartig für den Durchsatz, aber sehr sehr schwierig, richtig zu machen. Ein alternativer Ansatz zum Sperren, der durch Clojure eine gewisse Zugkraft gewinnt, wäre ein Software-Transaktionsspeicher (STM). Dies ist im Wesentlichen das Gegenteil von herkömmlichen Sperren, da es eher optimistisch als pessimistisch ist. Sie beginnen mit der Annahme, dass Sie keine Kollisionen haben, und erlauben dann dem Framework, die Probleme zu beheben, falls und wenn sie auftreten. Datenbanken funktionieren oft auf diese Weise. Es eignet sich hervorragend für den Durchsatz auf Systemen mit niedrigen Kollisionsraten, aber der große Gewinn liegt in der logischen Komponentenisierung Ihrer Algorithmen. Anstatt eine Sperre (oder eine Reihe von Sperren) willkürlich mit einigen Daten zu verknüpfen, wickeln Sie den gefährlichen Code einfach in eine Transaktion ein und lassen das Framework den Rest herausfinden. Sie können sogar ein bisschen Kompilierzeit bei der Überprüfung von anständigen STM-Implementierungen wie der STM-Monade von GHC oder meiner experimentellen Scala STM bekommen.
Es gibt viele neue Möglichkeiten, um gleichzeitige Anwendungen zu erstellen. Welche Sie auswählen, hängt stark von Ihrem Fachwissen, Ihrer Sprache und dem Problem ab, das Sie modellieren möchten. Generell denke ich, dass Akteure, die mit dauerhaften, unveränderlichen Datenstrukturen verbunden sind, eine solide Wette sind, aber wie gesagt, STM ist ein wenig weniger invasiv und kann manchmal unmittelbarere Verbesserungen bringen.
Ich empfehle die flussbasierte Programmierung, auch bekannt als Datenflussprogrammierung. Es verwendet OOP und Threads, ich fühle es wie einen natürlichen Schritt vorwärts, wie OOP zu prozeduralen war. Ich muss sagen, die Datenflussprogrammierung kann nicht für alles verwendet werden, sie ist nicht generisch.
Wikipedia hat gute Artikel zum Thema:
Außerdem hat es mehrere Vorteile, wie die unglaublich flexible Konfiguration, Layering; Der Programmierer (Component Programmer) muss die Geschäftslogik nicht programmieren, sondern in einer anderen Phase (das Verarbeitungsnetzwerk zusammensetzen).
Wussten Sie, dass make ein Datenflusssystem ist? Sehen Sie make -j , besonders wenn Sie einen Mehrkernprozessor haben.
Schreiben Sie den gesamten Code in einer Multithread-Anwendung sehr ... vorsichtig! Ich kenne keine bessere Antwort als das. (Dies beinhaltet Sachen wie jonnii erwähnt).
Ich habe gehört, dass Leute streiten (und ihnen zustimmen), dass das traditionelle Threading-Modell wirklich nicht in die Zukunft gehen wird, also werden wir eine andere Reihe von Paradigmen / Sprachen entwickeln müssen, um diese wirklich zu benutzen newfangled Multi-Cores effektiv. Sprachen wie Haskell, deren Programme leicht parallelisierbar sind, da jede Funktion, die Nebenwirkungen hat, explizit markiert werden muss, und Erlang, von dem ich leider nicht viel weiß.
Das Akteurmodell ist das, was Sie verwenden, und es ist bei weitem die einfachste (und effizienteste) Methode für Multithreading . Grundsätzlich hat jeder Thread eine (synchronisierte) Warteschlange (er kann vom Betriebssystem abhängig sein oder nicht) und andere Threads erzeugen Nachrichten und setzen sie in die Warteschlange des Threads, der die Nachricht behandeln wird.
Grundbeispiel:
%Vor%Es ist ein typisches Beispiel für Probleme mit Produzenten .
Es ist eindeutig ein schwieriges Problem. Abgesehen von dem offensichtlichen Bedürfnis nach Sorgfalt glaube ich, dass der erste Schritt darin besteht, genau zu definieren, welche Fäden Sie brauchen und warum.
Entwerfen Sie Threads so, wie Sie Klassen entwerfen würden: Stellen Sie sicher, dass Sie wissen, was sie konsistent macht: ihre Inhalte und ihre Interaktionen mit anderen Threads.
Als ich Java aus dem Hintergrund von 20 Jahren prozeduraler Programmierung mit Basic, Pascal, COBOL und C lernte, dachte ich damals, dass das Schwerste daran war, den OOP-Jargon und die Konzepte zu verstehen. Jetzt, mit ungefähr 8 Jahren solidem Java, bin ich zu dem Schluss gekommen, dass das Schwierigste an der Programmierung in Java und ähnlichen Sprachen wie C # die Multithreading / Concurrent-Aspekte sind.
Die Codierung zuverlässiger und skalierbarer Multithread-Anwendungen ist einfach schwer! Und mit dem Trend für Prozessoren, "breiter" statt schneller zu werden, wird es immer mehr zu einem kritischen Punkt.
Der schwierigste Bereich ist natürlich die Kontrolle der Interaktionen zwischen den Threads und den daraus resultierenden Fehlern: Deadlocks, Race Conditions, veraltete Daten und Latenz.
Meine Frage an Sie ist also: Welchen Ansatz oder welche Methodologie verwenden Sie , um gleichzeitig sicheren Code zu erzeugen und gleichzeitig das Potenzial für Deadlocks, Latenz und andere Probleme zu mindern? Ich habe einen Ansatz entwickelt, der etwas unkonventionell ist, aber in mehreren großen Anwendungen sehr gut funktioniert hat. Ich werde auf eine ausführliche Antwort auf diese Frage eingehen.
Ich schlage das Schauspielermodell vor.
Ich erinnere mich, dass ich etwas schockiert war, als ich entdeckte, dass Javas Klasse synchronizedList nicht vollständig threadsicher, sondern nur bedingt threadsicher war. Ich könnte immer noch gebrannt werden, wenn ich meine Zugriffe (Iteratoren, Setter usw.) nicht in einen synchronisierten Block verpacken würde. Dies bedeutet, dass ich meinem Team und meinem Management versichert habe, dass mein Code threadsicher ist, aber ich könnte mich geirrt haben. Eine andere Möglichkeit, Thread-Sicherheit zu gewährleisten, besteht darin, dass ein Tool den Code analysiert und ihn passieren lässt. STP, Actor Model, Erlang, etc. sind einige Möglichkeiten, die letztgenannte Form der Sicherheit zu erhalten. Die Fähigkeit, Eigenschaften eines Programms zuverlässig zu sichern, ist / wird ein großer Fortschritt in der Programmierung sein.
Sieht so aus, als wäre Ihr IOC etwas FBP-artig :-) Es wäre fantastisch, wenn der JavaFBP-Code von jemandem wie Ihnen, der in der Kunst des thread-sicheren Codes versiert ist, gründlich geprüft werden könnte ... Er ist auf SVN in SourceForge .
Einige Experten glauben, dass die Antwort auf Ihre Frage darin besteht, Threads gänzlich zu vermeiden, da es fast unmöglich ist, unvorhergesehene Probleme zu vermeiden. Um Das Problem mit Threads zu nennen:
Wir haben einen Prozess entwickelt, der Folgendes beinhaltet: ein Code-Reifegrad-Bewertungssystem (mit vier Ebenen, rot, gelb, grün und blau), Design-Reviews, Code Bewertungen, nächtliche Builds, Regressionstests und automatisierte Code Coverage-Metriken. Die Portion des Kernels, der für eine konsistente Sicht auf die Programmstruktur sorgte, wurde Anfang 2000 geschrieben, Das Design wurde auf Gelb und der Code auf Grün überprüft. Die Gutachter enthielten Nebenläufigkeitsexperten, nicht nur unerfahrene Studenten (Christopher Hylands (jetzt Brooks), Bart Kienhuis, John Reekie und [Ed Lee] waren alle Rezensenten). Wir haben Regressionstests geschrieben, die 100 Prozent Code erreichten Berichterstattung ... Das ... System selbst begann, weit verbreitet zu sein, und jeder Gebrauch des Systems übte dies aus Code. Keine Probleme wurden beobachtet, bis der Code am 26. April 2004, vier Jahre später, festgefahren war.
Die Kernanliegen waren, wie ich gesehen habe, (a) Vermeidung von Deadlocks und (b) Austausch von Daten zwischen Threads. Ein Vermieter Sorge (aber nur geringfügig Vermieter) war Engpässe zu vermeiden. Ich hatte bereits mehrere Probleme mit ungleicher Nicht-Sequenz-Sperre, die Deadlocks verursachte - es ist sehr gut zu sagen "immer Sperren in der gleichen Reihenfolge", aber in einem mittleren bis großen System ist es praktisch unmöglich, dies sicherzustellen.
>Vorbehalt: Als ich mit dieser Lösung kam, musste ich Java 1.1 ins Visier nehmen (also war das Nebenläufigkeitspaket in Doug Leas Augen noch kein Zwinkern) - die Werkzeuge waren vollständig synchronisiert und warteten / benachrichtigen. Ich habe Erfahrungen mit dem Schreiben eines komplexen Multi-Prozess-Kommunikationssystems mit dem Echtzeit-Nachrichtensystem QNX gemacht.
Basierend auf meiner Erfahrung mit QNX, die die Deadlock-Probleme hatte, aber Datenkonkurrenzen durch die Verarbeitung von Nachrichten aus dem Speicher eines Prozesses zu anderen vermeidet, habe ich einen auf Nachrichten basierenden Ansatz für Objekte entwickelt - den ich IOC genannt habe -Objektkoordination. Am Anfang dachte ich mir, ich könnte alle meine Objekte so erstellen, aber im Nachhinein stellt sich heraus, dass sie nur an den wichtigsten Kontrollpunkten in einer großen Anwendung notwendig sind - die "Interstate Interchange", wenn Sie wird nicht für jede einzelne "Kreuzung" im Straßensystem geeignet sein. Das stellt sich als großer Vorteil heraus, weil sie ganz und gar nicht POJO sind.
Ich stellte mir ein System vor, in dem Objekte nicht synchronisierte Methoden aufrufen, sondern "Nachrichten senden". Nachrichten können gesendet / beantwortet werden, wobei der Absender wartet, während die Nachricht verarbeitet wird, und mit der Antwort zurückkehrt, oder asynchron, wenn die Nachricht in einer Warteschlange gelöscht und zu einem späteren Zeitpunkt aus der Warteschlange entfernt und verarbeitet wird. Beachten Sie, dass dies eine konzeptionelle Unterscheidung ist - das Messaging wurde mithilfe synchronisierter Methodenaufrufe implementiert.
Die Kernobjekte für das Nachrichtensystem sind ein IsolatedObject, ein IocBinding und ein IocTarget.
Das IsolatedObject wird so genannt, weil es keine öffentlichen Methoden hat; Dies wird erweitert, um Nachrichten zu empfangen und zu verarbeiten. Durch die Verwendung von Reflektion wird zusätzlich erzwungen, dass das untergeordnete Objekt keine öffentlichen Methoden, noch ein Paket oder geschützte Methoden außer den von IsolatedObject vererbten hat, von denen fast alle endgültig sind; Es sieht zunächst sehr seltsam aus, denn wenn Sie IsolatedObject ableiten, erstellen Sie ein Objekt mit einer geschützten Methode:
%Vor%und alle anderen Methoden sind private Methoden, um bestimmte Nachrichten zu verarbeiten.
Das IocTarget ist ein Mittel, um die Sichtbarkeit eines IsolatedObject zu abstrahieren und ist sehr nützlich, um einem anderen Objekt eine Selbstreferenz zum Senden von Signalen an Sie zu geben, ohne Ihre tatsächliche Objektreferenz zu zeigen.
Und das IocBinding bindet einfach ein Absenderobjekt an einen Nachrichtenempfänger, so dass keine Validierungsprüfungen für jede gesendete Nachricht anfallen und mit einem IocTarget erstellt werden.
Jede Interaktion mit den isolierten Objekten erfolgt durch "Senden" von Nachrichten - die Methode processIocMessage des Empfängers wird synchronisiert, wodurch sichergestellt wird, dass jeweils nur eine Nachricht bearbeitet wird.
%Vor%Nachdem ich eine Situation geschaffen habe, in der alle Arbeiten des isolierten Objekts durch eine einzige Methode geleitet werden, ordnete ich die Objekte anschließend in einer deklarierten Hierarchie anhand einer "Klassifikation" an, die sie bei der Konstruktion deklarieren - einfach eine Zeichenfolge, die sie identifiziert eine von einer beliebigen Anzahl von "Arten von Nachrichtenempfängern" sein, die das Objekt innerhalb einer vorbestimmten Hierarchie platzieren. Dann habe ich den Nachrichtenübermittlungscode verwendet, um sicherzustellen, dass der Absender selbst ein IsolatedObject war, das für synchrone Sende- / Antwortnachrichten in der Hierarchie niedriger war. Asynchrone Nachrichten (Signale) werden an Nachrichtenempfänger gesendet, wobei separate Threads in einem Thread-Pool verwendet werden, deren gesamte Aufgabe Signale liefert, daher können Signale von jedem Objekt zu jedem Empfänger in dem System gesendet werden. Signale können beliebige Nachrichtendaten liefern, aber keine Antwort ist möglich.
Da Nachrichten nur in einer aufwärts -Richtung ausgegeben werden können (und Signale immer nach oben gerichtet sind, weil sie von einem separaten Thread geliefert werden, der nur für diesen Zweck ausgeführt wird), werden Deadlocks vom Design ausgeschlossen.
Da Interaktionen zwischen Threads durch den Austausch von Nachrichten mithilfe der Java-Synchronisation erreicht werden, werden Race Conditions und Probleme mit veralteten Daten ebenfalls vom Design ausgeschlossen.
Da jeder Empfänger nur eine Nachricht gleichzeitig verarbeitet und weil er keine anderen Eingangspunkte hat, werden alle Überlegungen zum Objektstatus eliminiert - effektiv ist das Objekt vollständig synchronisiert und die Synchronisation kann nicht versehentlich von irgendeiner Methode ausgelassen werden; keine Getter, die veraltete Cached-Thread-Daten zurückgeben, und keine Setter, die den Objektzustand ändern, während eine andere Methode darauf reagiert.
Da nur die Wechselwirkungen zwischen den Hauptkomponenten durch diesen Mechanismus geleitet werden, hat sich dieser in der Praxis sehr gut skalieren lassen - diese Interaktionen finden in der Praxis nicht annähernd so häufig statt wie ich theoretisiert habe.
Das gesamte Design wird zu einer geordneten Sammlung von Subsystemen , die streng kontrolliert interagieren.
Beachten Sie, dass dies nicht für einfachere Situationen verwendet wird, in denen Worker-Threads, die konventionellere Thread-Pools verwenden, ausreichen (obwohl ich die Ergebnisse des Arbeiters oft durch Senden einer IOC-Nachricht in das Hauptsystem zurückspeise). Es wird auch nicht für Situationen verwendet, in denen ein Thread abläuft und etwas völlig unabhängig vom Rest des Systems ausführt, beispielsweise ein HTTP-Server-Thread. Schließlich wird es nicht in Situationen verwendet, in denen es einen Ressourcen-Koordinator gibt, der selbst nicht mit anderen Objekten interagiert und bei dem die interne Synchronisation die Aufgabe ohne Deadlock-Risiko erledigt.
EDIT: Ich hätte sagen sollen, dass die ausgetauschten Nachrichten generell unveränderliche Objekte sein sollten; Wenn veränderbare Objekte verwendet werden, sollte der Vorgang des Sendens als Übergabe betrachtet werden, und der Absender soll alle Kontrolle aufgeben und vorzugsweise keine Referenzen auf die Daten behalten. Persönlich verwende ich eine abschließbare Datenstruktur, die durch den IOC-Code gesperrt ist und daher beim Senden unveränderlich wird (das Sperrflag ist flüchtig).
Der sicherste Ansatz zum Entwerfen neuer Anwendungen mit Multithreading besteht in der Einhaltung der folgenden Regel:
Kein Design unterhalb des Designs.
Was heißt das?
Stellen Sie sich vor, Sie haben wichtige Bausteine Ihrer Anwendung identifiziert. Lass es die GUI sein, einige Rechenmaschinen. Sobald Sie eine ausreichend große Teamgröße haben, werden einige Teammitglieder normalerweise nach "Bibliotheken" fragen, um Code zwischen diesen Hauptbausteinen auszutauschen. Während es anfänglich relativ einfach war, die Threading- und Kollaborationsregeln für die Hauptbausteine zu definieren, ist dieser Aufwand nun in Gefahr, da die "Code-Wiederverwendungsbibliotheken" schlecht entworfen, bei Bedarf entworfen und mit Sperren und Mutexen übersät sein werden "fühlt sich richtig an". Diese Ad-hoc-Bibliotheken sind das Design unterhalb Ihres Designs und das Hauptrisiko für Ihre Threading-Architektur.
Was ist damit zu tun?
Bedenken Sie nicht zuletzt, dass Sie einige nachrichtenbasierte Interaktionen zwischen Ihren Hauptbausteinen haben. siehe zum Beispiel das oft erwähnte Akteurmodell.