Wie erklärt man, warum Multithreading schwierig ist?

84

Ich bin ein ziemlich guter Programmierer, mein Chef ist auch ein ziemlich guter Programmierer. Obwohl er einige Aufgaben wie Multithreading zu unterschätzen scheint und wie schwierig es sein kann (ich finde es sehr schwierig, mehr als ein paar Threads auszuführen, darauf zu warten, dass alle fertig sind und dann Ergebnisse zurückgeben).

In dem Moment, in dem Sie sich Sorgen über Deadlocks und Rennbedingungen machen müssen, fällt es mir sehr schwer, aber der Chef scheint das nicht zu schätzen - ich glaube, er hat das noch nie erlebt. Einfach ein Schloss draufstecken ist so ziemlich die Einstellung.

Wie kann ich ihn also vorstellen oder erklären, warum er die Komplexität von Parallelität, Parallelität und Multithreading möglicherweise unterschätzt? Oder vielleicht irre ich mich?

Bearbeiten: Nur ein wenig von dem, was er getan hat - durchlaufen Sie eine Liste, und erstellen Sie für jedes Element in dieser Liste einen Thread, der einen Datenbankaktualisierungsbefehl basierend auf den Informationen in diesem Element ausführt. Ich bin mir nicht sicher, wie er gesteuert hat, wie viele Threads gleichzeitig ausgeführt wurden. Ich denke, er muss sie zu einer Warteschlange hinzugefügt haben, wenn zu viele ausgeführt wurden (er hätte kein Semaphor verwendet).

Herr Shoubs
quelle
17
Multithreading ist einfach. Die korrekte Synchronisation ist schwierig.
Vineet Reynolds
33
Bringen Sie drei Personen in den Raum, vorzugsweise mit unterschiedlichen Akzenten, und lassen Sie sie verschiedene, überlappende Teile des Problems der Parallelität erklären.
greyfade
Multithreading kann je nach Problem und Sprachunterstützung sehr schwierig oder sehr einfach sein. Clojure macht es einfach clojure.org/concurrent_programming
Job
4
@Job Die gleichzeitige Programmierung ist immer schwierig (in realen Projekten), unabhängig davon, welche Sprache Sie verwenden. Scala, Clojure oder Erlang machen es ein bisschen vernünftig, wenn Sie es mit Sprachen vergleichen möchten, die veränderbare Zustände verwenden und fördern.
Chiron
4
Meine Lieblingsmetapher dafür ist: "Würden Sie eine Schlaftablette und ein Abführmittel gleichzeitig nehmen?" Selbst komplexe Nachrichtenwarteschlangen verwenden, um die Frucht des gemeinsamen Zugriffs richtig gemacht . Das, es sei denn , Sie haben große Menge Erfahrung mit ihm, ist schwer für viele Menschen.
Tim Post

Antworten:

29
  1. Wenn Sie sich auf mathematische Erfahrungen verlassen können, veranschaulichen Sie, wie ein normaler Ausführungsfluss, der im Wesentlichen deterministisch ist, nicht nur nicht deterministisch mit mehreren Threads, sondern auch exponentiell komplex wird, da Sie sicherstellen müssen, dass jede mögliche Verschachtelung von Maschinenanweisungen immer noch das Richtige bewirkt. Ein einfaches Beispiel für ein verlorenes Update oder eine Situation mit unsauberen Lesevorgängen ist oft ein Augenöffner.

  2. "Slap a lock on it" ist die triviale Lösung ... sie löst alle Ihre Probleme, wenn Sie sich nicht um die Leistung sorgen. Versuchen Sie zu veranschaulichen, wie groß der Leistungseinbruch wäre, wenn beispielsweise Amazon die gesamte Ostküste sperren müsste, wenn jemand in Atlanta ein Buch bestellt!

Kilian Foth
quelle
1
+1 für die Erörterung der mathematischen Komplexität - auf diese Weise habe ich die Schwierigkeit der Parallelität von gemeinsam genutzten Zuständen verstanden und das Argument, das ich im Allgemeinen bei der Befürwortung von Nachrichtenübermittlungsarchitekturen anführe. -1 für "Schlagen Sie eine Sperre drauf" ... Der Ausdruck bedeutet einen unüberlegten Ansatz für die Verwendung von Sperren, der sehr wahrscheinlich zu einem Deadlock oder zu inkonsistentem Verhalten führt (da Clients Ihres Codes, die in verschiedenen Threads leben, Konflikte verursachen) Anforderungen, die jedoch nicht untereinander synchronisiert werden, verfügen die Clients über inkompatible Modelle des Status Ihrer Bibliothek.
Aidan Cully
2
Amazon muss während der Bearbeitung einer Bestellung den Bestand eines einzelnen Artikels in einem Lager für kurze Zeit sperren. Wenn ein bestimmter Artikel plötzlich sehr häufig bestellt wird, leidet die Bestellleistung für diesen Artikel, bis der Vorrat erschöpft ist und der Zugriff auf das Inventar schreibgeschützt ist (und daher zu 100% freigegeben werden kann). Eine Sache, die Amazon für andere Programme nicht tun will, ist die Möglichkeit, Bestellungen in die Warteschlange zu stellen, bis ein Lagerbestand erreicht ist, und die Option, Bestellungen in der Warteschlange zu bearbeiten, bevor ein Lagerbestand für neue Bestellungen verfügbar gemacht wird.
Blrfl
@Blrfl: Programme können dies tun, wenn sie für die Nachrichtenübermittlung über Warteschlangen geschrieben wurden. Es ist nicht erforderlich, dass alle Nachrichten an einen bestimmten Thread über eine einzelne Warteschlange gesendet werden.
Donal Fellows,
4
@Donal Fellows: Wenn 1 Million Widgets in einem Lager vorrätig sind und 1 Million Bestellungen zum gleichen Zeitpunkt eingehen, werden alle diese Anfragen auf einer bestimmten Ebene serialisiert, während die Artikel den Bestellungen zugeordnet werden, unabhängig davon, wie sie bearbeitet werden. Die praktische Realität ist, dass Amazon wahrscheinlich noch nie so viele Widgets auf Lager hat, dass die Verzögerung bei der Verarbeitung eines Auftragsstroms unannehmbar hoch wird, bevor der Lagerbestand aufgebraucht ist und allen anderen (parallel) gesagt werden kann: "Wir sind draußen. " Nachrichtenwarteschlangen sind eine großartige Möglichkeit, Deadlocks zu vermeiden, lösen jedoch nicht das Problem der starken Konkurrenz um eine begrenzte Ressource.
Blrfl
79

Multithreading ist einfach. Das Codieren einer Anwendung für Multithreading ist sehr, sehr einfach.

Es gibt einen einfachen Trick, und dies ist eine gut gestaltete Nachrichtenwarteschlange Verwendung ( nicht Ihre eigene Rolle) Daten zwischen Threads zu übergeben.

Das Schwierige ist, dass mehrere Threads ein freigegebenes Objekt auf magische Weise aktualisieren. Dann wird es fehleranfällig, weil die Leute nicht auf die Rennbedingungen achten, die vorhanden sind.

Viele Leute verwenden keine Nachrichtenwarteschlangen und versuchen, gemeinsam genutzte Objekte zu aktualisieren und Probleme für sich selbst zu schaffen.

Schwierig wird es, einen Algorithmus zu entwerfen, der gut funktioniert, wenn Daten zwischen mehreren Warteschlangen ausgetauscht werden. Das ist schwierig. Die Funktionsweise von nebeneinander vorhandenen Threads (über gemeinsam genutzte Warteschlangen) ist jedoch einfach.

Beachten Sie auch, dass Threads teilen I / O - Ressourcen. Es ist unwahrscheinlich, dass ein E / A-gebundenes Programm (dh Netzwerkverbindungen, Dateivorgänge oder Datenbankvorgänge) mit vielen Threads schneller läuft.

Wenn Sie das Problem der Aktualisierung gemeinsam genutzter Objekte veranschaulichen möchten, ist dies ganz einfach. Setzen Sie sich mit ein paar Papierkarten über den Tisch. Schreiben Sie eine einfache Reihe von Berechnungen auf - 4 oder 6 einfache Formeln - mit viel Platz auf der Seite.

Hier ist das Spiel. Sie lesen jeweils eine Formel, schreiben eine Antwort und legen eine Karte mit der Antwort ab.

Jeder von euch wird die halbe Arbeit machen, oder? Du bist in der Hälfte der Zeit fertig, oder?

Wenn Ihr Chef nicht viel nachdenkt und gerade erst anfängt, kommt es zu Konflikten, und beide schreiben Antworten auf dieselbe Formel. Das hat nicht geklappt, weil es eine inhärente Rassenbedingung zwischen Ihnen beiden gibt, die vor dem Schreiben lesen. Nichts hindert Sie daran, die gleiche Formel zu lesen und die Antworten des jeweils anderen zu überschreiben.

Es gibt viele, viele Möglichkeiten, Rennbedingungen mit schlecht oder nicht gesperrten Ressourcen zu schaffen.

Wenn Sie alle Konflikte vermeiden möchten, schneiden Sie das Papier in einen Stapel Formeln. Sie nehmen einen aus der Warteschlange, schreiben die Antwort auf und veröffentlichen die Antworten. Keine Konflikte, da Sie beide aus einer Nur-Lese-Nachrichtenwarteschlange lesen.

S.Lott
quelle
Selbst das Zerschneiden des Papiers zu einem Stapel behebt noch nicht alle Probleme - Sie haben immer noch die Situation, in der Sie und Ihr Chef gleichzeitig nach einer neuen Formel greifen und mit den Fingerknöcheln in seine schlagen. In der Tat würde ich sagen, dass dies für die häufigste Art von Threading-Problem repräsentativ ist. Die wirklich groben Fehler werden früh gefunden. Die wirklich ungewöhnlichen Fehler bleiben für immer bestehen, weil niemand sie reproduzieren kann. Die plausiblen Rennbedingungen - wie diese - tauchen immer wieder beim Testen auf, und schließlich werden alle (oder wahrscheinlich die meisten) ausgebügelt.
Airsource Ltd
@AirsourceLtd Was genau sagst du mit "Schlag mit den Fingerknöcheln in seine"? Solange Sie eine Nachrichtenwarteschlange haben, die verhindert, dass zwei unterschiedliche Threads dieselbe Nachricht empfangen, ist dies kein Problem. Es sei denn, ich verstehe falsch, was du meinst.
Zack
25

Multithread-Programmierung ist wahrscheinlich die schwierigste Lösung für die Parallelität. Es ist im Grunde genommen eine ziemlich einfache Abstraktion dessen, was die Maschine tatsächlich tut.

Es gibt eine Reihe von Ansätzen, wie das Akteurmodell oder das (Software-) Transaktionsgedächtnis , die viel einfacher sind. Oder mit unveränderlichen Datenstrukturen (wie Listen und Bäumen) arbeiten.

Im Allgemeinen erleichtert eine ordnungsgemäße Trennung von Bedenken das Multithreading. Etwas, das allzu oft vergessen wird, wenn Leute 20 Threads erzeugen und alle versuchen, den gleichen Puffer zu verarbeiten. Verwenden Sie Reaktoren, bei denen Sie eine Synchronisierung benötigen, und übergeben Sie im Allgemeinen Daten zwischen verschiedenen Mitarbeitern mit Nachrichtenwarteschlangen.
Wenn Sie eine Sperre in Ihrer Anwendungslogik haben, haben Sie etwas falsch gemacht.

Technisch gesehen ist Multithreading also schwierig.
"Slap a lock on it" ist so gut wie die am wenigsten skalierbare Lösung für Parallelitätsprobleme und macht den ganzen Zweck des Multithreading zunichte. Damit wird ein Problem auf ein nicht gleichzeitiges Ausführungsmodell zurückgesetzt. Je mehr Sie dies tun, desto wahrscheinlicher ist es, dass jeweils nur ein Thread ausgeführt wird (oder 0 in einem Deadlock). Es macht den ganzen Zweck zunichte.
Das ist so, als würde man sagen: "Die Probleme der 3. Welt zu lösen ist einfach. Wirf einfach eine Bombe darauf." Nur weil es eine triviale Lösung gibt, ist das Problem nicht trivial, da Sie auf die Qualität des Ergebnisses achten.

In der Praxis ist die Lösung dieser Probleme genauso schwierig wie alle anderen Programmierprobleme und wird am besten mit geeigneten Abstraktionen durchgeführt. Das macht es eigentlich ganz einfach.

back2dos
quelle
14

Ich denke, diese Frage hat einen nicht-technischen Aspekt - IMO geht es um Vertrauen. Wir werden häufig gebeten, komplexe Apps wie zum Beispiel Facebook zu reproduzieren. Ich bin zu dem Schluss gekommen, dass, wenn Sie dem Uneingeweihten / Management die Komplexität einer Aufgabe erklären müssen, in Dänemark etwas faul ist.

Selbst wenn andere Ninja-Programmierer die Aufgabe in 5 Minuten erledigen könnten, basieren Ihre Schätzungen auf Ihren persönlichen Fähigkeiten. Ihr Gesprächspartner sollte entweder lernen, Ihrer Meinung in dieser Angelegenheit zu vertrauen, oder jemanden einstellen, dessen Wort er zu akzeptieren bereit ist.

Die Herausforderung besteht nicht darin, die technischen Implikationen weiterzugeben, die die Menschen entweder eher ignorieren oder im Gespräch nicht erfassen können, sondern eine Beziehung des gegenseitigen Respekts aufzubauen.

sunwukung
quelle
1
Interessante Antwort, obwohl es eine technische Frage ist. Ich stimme jedoch Ihrer Aussage zu ... in diesem Fall ist mein Manager ein ziemlich guter Programmierer, aber ich denke nur, dass er die Komplexität von Multithread-Apps nicht erkannt hat, sondern sie unterschätzt.
Herr Shoubs
6

Ein einfaches Gedankenexperiment zum Verständnis von Deadlocks ist das Problem des " Essensphilosophen ". Eines der Beispiele, mit denen ich normalerweise beschreibe, wie schlecht die Rennbedingungen sein können, ist die Therac 25- Situation.

"Einfach eine Sperre draufstecken" ist die Mentalität von jemandem, der nicht auf schwierige Bugs mit Multithreading gestoßen ist. Und es ist möglich, dass er denkt , dass Sie den Ernst der Situation überbewerten (ich nicht - es ist möglich, Dinge in die Luft zu jagen oder Menschen mit Race Condition Bugs zu töten, insbesondere mit eingebetteter Software, die in Autos landet).

Tangurena
quelle
1
dh das Sandwich-Problem: Sie machen einen Haufen Sandwiches, aber es gibt nur 1 Butterdose und 1 Messer. Im Allgemeinen ist alles in Ordnung, aber irgendwann greift jemand nach der Butter, während ein anderer nach dem Messer greift. Dann stehen beide da und warten darauf, dass der andere ihre Ressource loslässt.
gbjbaanb
Könnten Deadlock-Probleme wie diese gelöst werden, indem Ressourcen immer in einer festgelegten Reihenfolge beschafft werden?
Compman
@compman, nein. Es ist nämlich möglich, dass zwei Threads gleichzeitig versuchen, nach derselben Ressource zu greifen, und diese Threads benötigen nicht unbedingt denselben Satz von Ressourcen - nur eine Überlappung, die Probleme verursacht. Ein Schema besteht darin, die Ressource "zurückzusetzen" und dann auf eine zufällige Zeitspanne zu warten, bevor erneut danach gegriffen wird. Diese Backoff-Phase findet in einer Reihe von Protokollen statt, von denen Aloha das erste war. en.wikipedia.org/wiki/ALOHAnet
Tangurena
1
Was wäre, wenn jede Ressource im Programm eine Nummer hätte und wenn ein Thread / Prozess eine Reihe von Ressourcen benötigt, würde er die Ressourcen immer in aufsteigender numerischer Reihenfolge sperren? Ich glaube nicht, dass ein Deadlock passieren könnte.
Compman
1
@compman: Das ist in der Tat ein Weg, um Deadlocks zu vermeiden. Es ist möglich, Tools zu entwerfen, mit denen Sie dies automatisch überprüfen können. Wenn sich herausstellt, dass Ihre Anwendung niemals andere Ressourcen als in aufsteigender numerischer Reihenfolge sperrt, ist möglicherweise kein Deadlock aufgetreten. (Beachten Sie, dass potenzielle Deadlocks nur dann zu echten Deadlocks werden, wenn Ihr Code auf dem Computer eines Kunden ausgeführt wird.)
gnasher729
3

Gleichzeitige Anwendungen sind nicht deterministisch. Mit der außergewöhnlich geringen Menge an Gesamtcode, die der Programmierer als anfällig erkannt hat, können Sie nicht steuern, wann ein Teil eines Threads / Prozesses in Bezug auf einen Teil eines anderen Threads ausgeführt wird. Das Testen ist schwieriger, dauert länger und es ist unwahrscheinlich, dass alle Fehler im Zusammenhang mit der Parallelität gefunden werden. Wenn Defekte gefunden werden, die dann subtil sind, können sie nicht konsistent reproduziert werden, weshalb das Reparieren schwierig ist.

Daher ist die einzige richtige gleichzeitige Anwendung eine, die nachweislich richtig ist, was in der Softwareentwicklung nicht oft praktiziert wird. Infolgedessen ist die Antwort von S.Lot der beste allgemeine Rat, da die Weitergabe von Nachrichten relativ einfach als richtig zu beweisen ist.

mattnz
quelle
3

Kurze Antwort in zwei Worten: BEOBACHTBARER NONDETERMINISMUS

Lange Antwort: Es hängt davon ab, welchen Ansatz für die gleichzeitige Programmierung Sie bei Ihrem Problem verwenden. In dem Buch Konzepte, Techniken und Modelle der Computerprogrammierung erläutern die Autoren vier praktische Hauptansätze für das Schreiben von parallelen Programmen:

  • Sequentielle Programmierung : Ein Baseline-Ansatz, bei dem keine Parallelität besteht.
  • Deklarative Nebenläufigkeit : verwendbar, wenn kein beobachtbarer Nichtdeterminismus vorliegt;
  • Message-Passing - Parallelität : gleichzeitige Nachricht zwischen vielen Einheiten vorbei, wobei jede Einheit der Nachricht intern sequentiell verarbeiten;
  • Shared State Concurrency : Thread-Aktualisierung gemeinsam genutzter passiver Objekte durch grobkörnige atomare Aktionen, z. B. Sperren, Monitore und Transaktionen;

Der einfachste dieser vier Ansätze, abgesehen von der offensichtlichen sequentiellen Programmierung, ist die deklarative Parallelität , da die mit diesem Ansatz geschriebenen Programme keinen beobachtbaren Nichtdeterminismus aufweisen . Mit anderen Worten, es gibt keine Rennbedingungen , da die Rennbedingungen nur ein beobachtbares nicht deterministisches Verhalten sind.

Das Fehlen eines beobachtbaren Nichtdeterminismus bedeutet jedoch, dass es einige Probleme gibt, die wir mit deklarativer Parallelität nicht lösen können. Hier kommen die letzten beiden nicht ganz einfachen Ansätze ins Spiel. Der nicht so einfache Teil ist eine Folge des beobachtbaren Nichtdeterminismus. Jetzt fallen beide unter das Stateful Concurrent-Modell und sind auch in ihrer Ausdruckskraft gleichwertig. Aufgrund der ständig wachsenden Anzahl von Kernen pro CPU scheint sich die Branche jedoch in letzter Zeit mehr für die gleichzeitige Übermittlung von Nachrichten interessiert zu haben, wie dies an der Zunahme von Bibliotheken für die Nachrichtenübermittlung (z. B. Akka für JVM) oder Programmiersprachen (z. B. Erlang ) zu erkennen ist. .

Die zuvor erwähnte Akka-Bibliothek, die auf einem theoretischen Actor-Modell basiert, vereinfacht das Erstellen von gleichzeitigen Anwendungen, da Sie sich nicht mehr mit Sperren, Monitoren oder Transaktionen befassen müssen. Zum anderen erfordert es einen anderen Lösungsansatz, nämlich das Denken, wie Akteure hierarchisch zusammengesetzt werden können. Man könnte sagen, dass es eine völlig andere Denkweise erfordert, die am Ende sogar noch schwieriger sein kann als die Verwendung von gemeinsam genutzter Parallelität im Klartext.

Die gleichzeitige Programmierung ist aufgrund des beobachtbaren Nichtdeterminismus schwierig , aber wenn der richtige Ansatz für das gegebene Problem und die richtige Bibliothek, die diesen Ansatz unterstützt, verwendet wird, können viele Probleme vermieden werden.

Jernej Jerin
quelle
0

Mir wurde zuerst beigebracht, dass es Probleme aufwerfen könnte, indem man ein einfaches Programm sah, das 2 Threads startete und beide von 1-100 gleichzeitig auf die Konsole druckten. Anstatt von:

1
1
2
2
3
3
...

Sie erhalten etwas mehr davon:

1
2
1
3
2
3
...

Führen Sie es erneut aus, und Sie erhalten möglicherweise ganz andere Ergebnisse.

Die meisten von uns sind darauf vorbereitet, dass unser Code nacheinander ausgeführt wird. Bei den meisten Multithreading-Verfahren können wir dies nicht als "out of the box" voraussetzen.

Morgan Herlocker
quelle
-3

Versuchen Sie, mehrere Hämmer zu verwenden, um ein Bündel eng beieinander liegender Nägel gleichzeitig zu zerdrücken, ohne dass sich jemand mit den Hämmern verständigt.

Eskaliere dies, um ein Haus zu bauen.

Versuche nachts zu schlafen, wenn du der Architekt bist. :)

Macke
quelle
-3

Einfacher Teil: Verwenden Sie Multithreading mit zeitgemäßen Features von Frameworks, Betriebssystemen und Hardware, wie Semaphoren, Warteschlangen, Interlocked Counters, Atomic Boxed Types usw.

Schwieriger Teil: Implementieren Sie die Features selbst, indem Sie zunächst keine Features verwenden. Möglicherweise gibt es nur wenige sehr eingeschränkte Hardwarefunktionen, die sich beispielsweise nur auf die Gewährleistung der Taktkohärenz für mehrere Kerne stützen.


quelle
3
Der schwierige Teil ist in der Tat schwieriger, aber selbst dieser einfache Teil ist nicht so einfach.
PeterAllenWebb