Welche Lehren haben Sie aus einem Projekt gezogen, das aufgrund von schlechtem Multithreading fast / tatsächlich gescheitert ist? [geschlossen]

11

Welche Lehren haben Sie aus einem Projekt gezogen, das aufgrund von schlechtem Multithreading fast / tatsächlich gescheitert ist?

Manchmal schreibt das Framework ein bestimmtes Threading-Modell vor, das es schwieriger macht, Dinge um eine Größenordnung richtig zu machen.

Ich habe mich noch nicht von dem letzten Fehler erholt und bin der Meinung, dass es für mich besser ist, an nichts zu arbeiten, was mit Multithreading in diesem Rahmen zu tun hat.

Ich fand, dass ich gut in Multithreading-Problemen war, die eine einfache Verzweigung / Verbindung haben und bei denen Daten nur in eine Richtung übertragen werden (während Signale in kreisförmiger Richtung übertragen werden können).

Ich kann keine GUI verarbeiten, in der einige Arbeiten nur an einem streng serialisierten Thread (dem "Hauptthread") und andere Arbeiten nur an einem anderen Thread als dem Hauptthread (den "Arbeitsthreads") ausgeführt werden können Dabei müssen Daten und Nachrichten zwischen N Komponenten in alle Richtungen übertragen werden (ein vollständig verbundener Graph).

Zu der Zeit, als ich dieses Projekt für ein anderes verließ, gab es überall Deadlock-Probleme. Ich habe gehört, dass es 2-3 Monate später mehreren anderen Entwicklern gelungen ist, alle Deadlock-Probleme zu beheben, bis sie an Kunden versendet werden können. Ich habe es nie geschafft herauszufinden, dass mir das fehlende Wissen fehlt.

Etwas über das Projekt: Die Anzahl der Nachrichten-IDs (ganzzahlige Werte, die die Bedeutung eines Ereignisses beschreiben, das unabhängig vom Threading in die Nachrichtenwarteschlange eines anderen Objekts gesendet werden kann) beträgt mehrere Tausend. Eindeutige Zeichenfolgen (Benutzernachrichten) sind ebenfalls ungefähr tausend.

Hinzugefügt

Die beste Analogie, die ich von einem anderen Team erhalten habe (unabhängig von meinen früheren oder gegenwärtigen Projekten), war, "die Daten in eine Datenbank zu stellen". ("Datenbank" bezieht sich auf Zentralisierung und atomare Aktualisierungen.) In einer GUI, die in mehrere Ansichten fragmentiert ist, die alle auf demselben "Hauptthread" ausgeführt werden, und das gesamte Nicht-GUI-Schwergewicht in einzelnen Arbeitsthreads ausgeführt wird, sollten die Daten der Anwendung verwendet werden in einer einzigen Plase gespeichert werden, die sich wie eine Datenbank verhält, und die "Datenbank" alle "atomaren Aktualisierungen" mit nicht trivialen Datenabhängigkeiten verarbeiten lassen. Alle anderen Teile der GUI behandeln nur das Zeichnen von Bildschirmen und sonst nichts. Die UI-Teile könnten Dinge zwischenspeichern und der Benutzer wird nicht bemerken, ob sie im Bruchteil einer Sekunde veraltet sind, wenn sie richtig entworfen wurden. Diese "Datenbank" wird auch als "Dokument" bezeichnet. in der Document-View-Architektur. Leider - nein, meine App speichert tatsächlich alle Daten in den Ansichten. Ich weiß nicht, warum es so war.

Mitwirkende:

(Mitwirkende müssen keine realen / persönlichen Beispiele verwenden. Lehren aus anekdotischen Beispielen sind ebenfalls willkommen, wenn sie von Ihnen als glaubwürdig beurteilt werden.)

rwong
quelle
Nicht lesen, was jeder
Entwickler
Ich denke, in Fäden denken zu können, ist ein Talent und weniger etwas, das man lernen kann, weil es keine bessere Formulierung gibt. Ich kenne viele Entwickler, die schon sehr lange mit parallelen Systemen arbeiten, aber sie ersticken, wenn die Daten in mehr als eine Richtung gehen müssen.
Dauphic

Antworten:

13

Meine Lieblingsstunde - sehr hart gewonnen! - Ist das in einem Multithread-Programm der Scheduler ein hinterhältiges Schwein, das Sie hasst? Wenn etwas schief gehen kann, werden sie es tun, aber auf unerwartete Weise. Wenn Sie etwas falsch machen, werden Sie seltsamen Heisenbugs nachjagen (weil jede Instrumentierung, die Sie hinzufügen, die Timings ändert und Ihnen ein anderes Laufmuster gibt).

Die einzig vernünftige Möglichkeit, dies zu beheben, besteht darin, die gesamte Thread-Handhabung streng in einen so kleinen Code umzuwandeln, der alles in Ordnung bringt und sehr konservativ ist, um sicherzustellen, dass die Sperren ordnungsgemäß gehalten werden (und dies auch bei einer global konstanten Reihenfolge der Erfassung). . Der einfachste Weg, dies zu tun, besteht darin, den Speicher (oder andere Ressourcen) nicht zwischen Threads zu teilen, außer für Nachrichten, die asynchron sein müssen. Auf diese Weise können Sie alles andere in einem Stil schreiben, der keine Threads enthält. (Bonus: Das Skalieren auf mehrere Computer in einem Cluster ist viel einfacher.)

Donal Fellows
quelle
+1 für "Speicher (oder andere Ressourcen) nicht zwischen Threads gemeinsam nutzen, außer für Nachrichten, die asynchron sein müssen;"
Nemanja Trifunovic
1
Der einzige Weg? Was ist mit unveränderlichen Datentypen?
Aaronaught
is that in a multithreaded program the scheduler is a sneaky swine that hates you.- Nein, tut es nicht, es macht genau das, was du ihm gesagt hast :)
Mattnz
@Aaronaught: Globale Werte, die als Referenz übergeben werden, erfordern, auch wenn sie unveränderlich sind, immer noch globale GC, und dies führt eine ganze Reihe globaler Ressourcen wieder ein. Die Möglichkeit, die Speicherverwaltung pro Thread zu verwenden, ist hilfreich, da Sie damit eine ganze Reihe globaler Sperren entfernen können.
Donal Fellows
Es ist nicht so, dass Sie Werte nicht grundlegender Typen nicht als Referenz übergeben können, sondern dass höhere Sperrstufen erforderlich sind (z. B. wenn der „Eigentümer“ eine Referenz hält, bis eine Nachricht zurückkommt, was bei der Wartung leicht durcheinander gebracht werden kann). oder komplexer Code in der Messaging-Engine, um das Eigentum zu übertragen. Oder Sie marshallen alles und marshallen im anderen Thread, was viel langsamer ist (das müssen Sie sowieso tun, wenn Sie zu einem Cluster gehen). Es ist einfacher, auf den Punkt zu kommen und überhaupt keinen Speicher zu teilen.
Donal Fellows
6

Hier sind einige grundlegende Lektionen, die ich mir gerade vorstellen kann (nicht aus fehlgeschlagenen Projekten, sondern aus realen Problemen, die bei realen Projekten auftreten):

  • Vermeiden Sie blockierende Anrufe, während Sie eine gemeinsam genutzte Ressource halten. Häufiges Deadlock-Muster ist Thread Grabs Mutex, macht einen Rückruf, Callback-Blöcke auf demselben Mutex.
  • Schützen Sie den Zugriff auf gemeinsam genutzte Datenstrukturen mit einem Mutex / kritischen Abschnitt (oder verwenden Sie sperrenfreie - aber erfinden Sie keine eigenen!)
  • Nehmen Sie keine Atomizität an - verwenden Sie atomare APIs (z. B. InterlockedIncrement).
  • RTFM zur Thread-Sicherheit von Bibliotheken, Objekten oder APIs, die Sie verwenden.
  • Nutzen Sie die verfügbaren Synchonisierungsprimitive, z. B. Ereignisse, Semaphoren. (Aber achten Sie genau darauf, wenn Sie sie verwenden, von denen Sie wissen, dass Sie sich in einem guten Zustand befinden. Ich habe viele Beispiele für Ereignisse gesehen, die im falschen Zustand signalisiert wurden, sodass Ereignisse oder Daten verloren gehen können.)
  • Angenommen, Threads können gleichzeitig und / oder in beliebiger Reihenfolge ausgeführt werden, und dieser Kontext kann jederzeit zwischen Threads wechseln (es sei denn, es handelt sich um ein Betriebssystem, das andere Garantien gibt).
Guy Sirton
quelle
6
  • Ihr gesamtes GUI- Projekt sollte nur vom Hauptthread aus aufgerufen werden . Grundsätzlich sollten Sie keinen einzigen (.net) "Aufruf" in Ihre GUI einfügen. Multithreading sollte in separaten Projekten stecken bleiben, die den langsameren Datenzugriff handhaben.

Wir haben einen Teil geerbt, in dem das GUI-Projekt ein Dutzend Threads verwendet. Es gibt nichts als Probleme. Deadlocks, Rennprobleme, Cross-Thread-GUI-Aufrufe ...

Carra
quelle
Bedeutet "Projekt" "Montage"? Ich sehe nicht ein, wie die Verteilung von Klassen auf Assemblys Threading-Probleme verursachen würde.
Nikie
In meinem Projekt ist es in der Tat eine Versammlung. Der wichtigste Punkt ist jedoch, dass der gesamte Code in diesen Ordnern ausnahmslos vom Hauptthread aufgerufen werden muss.
Carra
Ich denke nicht, dass diese Regel allgemein anwendbar ist. Ja, Sie sollten niemals GUI-Code von einem anderen Thread aufrufen. Es ist jedoch eine unabhängige Entscheidung, wie Sie Klassen auf Ordner / Projekte / Assemblys verteilen.
Nikie
1

Java 5 und höher verfügt über Executoren, die das Handling von Programmen im Fork-Join-Stil mit mehreren Threads vereinfachen sollen.

Verwenden Sie diese, es wird viel von dem Schmerz entfernen.

(und ja, das habe ich aus einem Projekt gelernt :))


quelle
1
Um diese Antwort auf andere Sprachen anzuwenden, verwenden Sie nach Möglichkeit hochwertige Parallelverarbeitungs-Frameworks, die von dieser Sprache bereitgestellt werden. (Allerdings wird nur die Zeit zeigen, ob ein Framework wirklich großartig und sehr
benutzerfreundlich ist
1

Ich habe einen Hintergrund in eingebetteten Echtzeitsystemen. Sie können nicht testen, ob keine Probleme durch Multithreading auftreten. (Sie können manchmal die Anwesenheit bestätigen). Der Code muss nachweislich korrekt sein. Best Practice für alle Thread-Interaktionen.

  • Regel Nr. 1: KISS - Wenn Sie keinen Thread benötigen, drehen Sie keinen. Serialisieren Sie so viel wie möglich.
  • Regel Nr. 2: Brechen Sie nicht Nr. 1.
  • # 3 Wenn Sie nicht durch Überprüfung beweisen können, dass es korrekt ist, ist es nicht.
mattnz
quelle
+1 für Regel 1. Ich habe an einem Projekt gearbeitet, das zunächst blockiert werden sollte, bis ein anderer Thread abgeschlossen war - im Wesentlichen ein Methodenaufruf! Zum Glück haben wir uns gegen diesen Ansatz entschieden.
Michael K
# 3 FTW. Es ist besser, Stunden damit zu verbringen, mit Sperrzeitdiagrammen oder was auch immer Sie verwenden, um zu beweisen, dass es gut ist, als Monate damit zu verbringen, sich zu fragen, warum es manchmal auseinander fällt.
1

Eine Analogie aus einem Multithreading-Kurs, den ich letztes Jahr besucht habe, war sehr hilfreich. Die Thread-Synchronisation ist wie ein Verkehrssignal, das eine Kreuzung (Daten) davor schützt, von zwei Autos (Threads) gleichzeitig verwendet zu werden. Der Fehler, den viele Entwickler machen, besteht darin, die Lichter in den meisten Teilen der Stadt rot zu machen, um ein Auto durchzulassen, weil sie der Meinung sind, dass es zu schwierig oder gefährlich ist, das genaue Signal herauszufinden, das sie benötigen. Dies funktioniert möglicherweise gut, wenn der Datenverkehr gering ist, führt jedoch zu einem Stillstand, wenn Ihre Anwendung wächst.

Das wusste ich theoretisch bereits, aber nach diesem Kurs blieb mir die Analogie wirklich erhalten, und ich war erstaunt, wie oft ich danach ein Threading-Problem untersuchte und eine riesige Warteschlange fand oder Interrupts während eines Schreibvorgangs in eine Variable überall deaktiviert wurden Es wurden nur zwei Threads verwendet oder Mutexe wurden lange gehalten, wenn sie überarbeitet werden konnten, um sie insgesamt zu vermeiden.

Mit anderen Worten, einige der schlimmsten Threading-Probleme werden durch Overkill verursacht, der versucht, Threading-Probleme zu vermeiden.

Karl Bielefeldt
quelle
0

Versuchen Sie es erneut.

Zumindest für mich war das Üben ein Unterschied. Nachdem Sie einige Male Multithread- und verteilte Arbeiten ausgeführt haben, haben Sie einfach den Dreh raus.

Ich denke, das Debuggen macht es wirklich schwierig. Ich kann Multithread-Code mit VS debuggen, aber ich bin wirklich ratlos, wenn ich gdb verwenden muss. Wahrscheinlich meine Schuld.

Eine andere Sache, über die Sie mehr lernen, sind sperrfreie Datenstrukturen.

Ich denke, diese Frage kann wirklich verbessert werden, wenn Sie das Framework angeben. Beispielsweise unterscheiden sich .NET-Thread-Pools und Hintergrund-Worker erheblich von QThread. Es gibt immer ein paar plattformspezifische Fallstricke.

Vitor Py
quelle
Ich bin daran interessiert, Geschichten aus beliebigen Frameworks zu hören, weil ich glaube, dass es von jedem Framework etwas zu lernen gibt, insbesondere solche, denen ich nicht ausgesetzt war.
Rwong
1
Debugger sind in einer Multithread-Umgebung weitgehend nutzlos.
Pemdas
Ich habe bereits Multithread-Ausführungs-Tracer, die mir das Problem mitteilen, mir aber nicht bei der Lösung helfen. Der Kern meines Problems ist, dass "gemäß dem aktuellen Design die Nachricht X nicht auf diese Weise (Sequenz) an das Objekt Y übergeben werden kann; sie muss zu einer riesigen Warteschlange hinzugefügt werden und wird schließlich verarbeitet; aber aus diesem Grund Es gibt keine Möglichkeit, dass Nachrichten dem Benutzer zum richtigen Zeitpunkt angezeigt werden. Dies geschieht immer anachronistisch und macht den Benutzer sehr, sehr verwirrt. Möglicherweise müssen Sie sogar Fortschrittsbalken, Abbrechen-Schaltflächen oder Fehlermeldungen an Stellen hinzufügen, die nicht angezeigt werden sollten. Ich habe die nicht . "
Rwong
0

Ich habe gelernt, dass Rückrufe von Modulen niedrigerer Ebene zu Modulen höherer Ebene ein großes Übel sind, weil sie dazu führen, dass Sperren in umgekehrter Reihenfolge erworben werden.

Sergej Zagursky
quelle
Rückrufe sind nicht böse ... die Tatsache, dass sie etwas anderes als Fadenbruch tun, ist wahrscheinlich die Wurzel des Bösen. Ich würde jeden Rückruf verdächtigen, der nicht nur ein Token an die Nachrichtenwarteschlange gesendet hat.
Pemdas
Das Lösen eines Optimierungsproblems (wie das Minimieren von f (x)) wird häufig implementiert, indem der Zeiger auf eine Funktion f (x) für die Optimierungsprozedur bereitgestellt wird, die es "zurückruft", während nach dem Minimum gesucht wird. Wie würden Sie es ohne Rückruf machen?
quant_dev
1
Keine Ablehnung, aber Rückrufe sind nicht böse. Einen Rückruf zu rufen, während Sie ein Schloss halten, ist böse. Rufen Sie nichts in einem Schloss an, wenn Sie nicht wissen, ob es sperren oder warten könnte. Dies umfasst nicht nur Rückrufe, sondern auch virtuelle Funktionen, API-Funktionen und Funktionen in anderen Modulen ("höhere Ebene" oder "niedrigere Ebene").
Nikie
@nikie: Wenn während des Rückrufs eine Sperre gehalten werden muss , muss entweder der Rest der API so ausgelegt sein, dass er wiedereintritt (schwer!), oder die Tatsache, dass Sie eine Sperre halten, muss ein dokumentierter Teil der API sein ( unglücklich, aber manchmal alles, was Sie tun können).
Donal Fellows
@Donal Fellows: Wenn während eines Rückrufs eine Sperre gehalten werden muss, würde ich sagen, dass Sie einen Konstruktionsfehler haben. Wenn es wirklich keinen anderen Weg gibt, dann ja, auf jeden Fall dokumentieren! Genau wie Sie dokumentieren würden, ob der Rückruf in einem Hintergrundthread aufgerufen wird. Das ist Teil der Schnittstelle.
Nikie