FP-Befürworter haben behauptet, dass Parallelität einfach ist, weil ihr Paradigma einen veränderlichen Zustand vermeidet. Ich verstehe es nicht.
Stellen Sie sich vor, wir erstellen mit FP einen Multiplayer-Dungeon-Crawl (ein Roguelike), bei dem reine Funktionen und unveränderliche Datenstrukturen im Vordergrund stehen. Wir erzeugen einen Kerker, der aus Räumen, Gängen, Helden, Monstern und Beute besteht. Unsere Welt ist quasi ein Objektgraph von Strukturen und ihren Beziehungen. Wenn sich die Dinge ändern, wird unsere Darstellung der Welt geändert, um diese Änderungen widerzuspiegeln. Unser Held tötet eine Ratte, greift nach einem Kurzschwert usw.
Für mich trägt die Welt (aktuelle Realität) diese Staatsidee und ich vermisse, wie FP diese überwindet. Während unser Held handelt, verändern Funktionen den Zustand der Welt. Es scheint, dass jede Entscheidung (KI oder Mensch) auf dem gegenwärtigen Zustand der Welt basieren muss. Wo würden wir Parallelität zulassen? Es können nicht mehrere Prozesse gleichzeitig den Zustand der Welt verbessern, damit ein Prozess seine Ergebnisse nicht auf einen abgelaufenen Zustand stützt. Meiner Meinung nach sollte die gesamte Steuerung in einem einzigen Regelkreis erfolgen, damit wir immer den aktuellen Zustand verarbeiten, der durch unseren aktuellen Objektgraphen der Welt dargestellt wird.
Offensichtlich gibt es Situationen, die perfekt für die Parallelität geeignet sind (z. B. bei der Verarbeitung von isolierten Aufgaben, deren Status unabhängig voneinander sind).
Ich verstehe nicht, wie nützlich Parallelität in meinem Beispiel ist, und das kann das Problem sein. Ich kann die Behauptung irgendwie falsch darstellen.
Kann jemand diesen Anspruch besser vertreten?
quelle
Antworten:
Ich werde versuchen, auf die Antwort hinzuweisen. Dies ist keine Antwort, nur eine einleitende Illustration. @jks Antwort zeigt auf die reale Sache, Reißverschlüsse.
Stellen Sie sich vor, Sie haben eine unveränderliche Baumstruktur. Sie möchten einen Knoten ändern, indem Sie ein untergeordnetes Element einfügen. Als Ergebnis erhalten Sie einen ganz neuen Baum.
Aber der größte Teil des neuen Baums ist genau der gleiche wie der alte Baum. Eine clevere Implementierung würde die meisten Baumfragmente wiederverwenden und Zeiger um den geänderten Knoten herum routen:
Okasakis Buch ist voller solcher Beispiele.
Ich nehme an, Sie können kleine Teile Ihrer Spielwelt bei jeder Bewegung angemessen verändern (eine Münze aufheben) und nur kleine Teile Ihrer Weltdatenstruktur (die Zelle, in der die Münze aufgehoben wurde) tatsächlich verändern. Teile, die nur zu früheren Staaten gehören, werden rechtzeitig mit dem Müll gesammelt.
Dies berücksichtigt wahrscheinlich einige Aspekte bei der angemessenen Gestaltung der Struktur der Datenspielwelt. Leider bin ich kein Experte in diesen Angelegenheiten. Auf jeden Fall muss es etwas anderes als eine NxM-Matrix sein, die man als veränderbare Datenstruktur verwenden würde. Wahrscheinlich sollte es aus kleineren Teilen bestehen (Korridore? Einzelne Zellen?), Die wie Baumknoten aufeinander zeigen.
quelle
Die Antwort von 9000 ist die halbe Antwort. Persistente Datenstrukturen ermöglichen es Ihnen, unveränderte Teile wiederzuverwenden.
Sie denken aber vielleicht schon: "Hey, was ist, wenn ich die Wurzel des Baumes wechseln möchte?" In dem gegebenen Beispiel bedeutet dies nun, dass alle Knoten geändert werden. Hier kommen Reißverschlüsse zum Einsatz. Mit ihnen kann das Element eines Fokus in O (1) geändert und der Fokus an eine beliebige Stelle in der Struktur verschoben werden.
Der andere Punkt bei Reißverschlüssen ist, dass es einen Reißverschluss für so ziemlich jeden gewünschten Datentyp gibt
quelle
Programme im funktionalen Stil bieten viele Möglichkeiten, die Parallelität zu nutzen. Jedes Mal, wenn Sie eine Sammlung transformieren, filtern oder aggregieren und alles rein oder unveränderlich ist, besteht die Möglichkeit, dass der Vorgang durch Parallelität beschleunigt wird.
Angenommen, Sie treffen AI-Entscheidungen unabhängig voneinander und in keiner bestimmten Reihenfolge. Sie wechseln sich nicht ab, sie treffen alle gleichzeitig eine Entscheidung und dann schreitet die Welt voran. Der Code könnte so aussehen:
Sie haben eine Funktion, mit der Sie berechnen können, was ein Monster in einem bestimmten Weltzustand tun wird, und die Sie auf jedes Monster anwenden können, um den nächsten Weltzustand zu berechnen. Dies ist in einer funktionalen Sprache eine Selbstverständlichkeit, und der Compiler kann den Schritt "Auf jedes Monster anwenden" parallel ausführen.
In einer imperativen Sprache würden Sie mit größerer Wahrscheinlichkeit jedes Monster durchlaufen und seine Auswirkungen auf die Welt anwenden. Dies ist nur einfacher, da Sie sich nicht mit Klonen oder kompliziertem Aliasing befassen möchten. Der Compiler kann in diesem Fall die Monsterberechnungen nicht parallel ausführen, da frühe Monsterentscheidungen sich auf spätere Monsterentscheidungen auswirken.
quelle
Hören ein paar Reiche Hickey spricht - diese insbesondere - meine Verwirrung gelindert. In einem hat er angegeben, dass es in Ordnung ist, dass gleichzeitig ablaufende Prozesse möglicherweise nicht den aktuellsten Status haben. Das musste ich hören. Was mir schwerfiel, war, dass Programme tatsächlich in Ordnung wären, wenn sie Entscheidungen auf Momentaufnahmen der Welt stützen würden, die inzwischen von neueren abgelöst wurden. Ich habe mich immer wieder gefragt, wie es mit der gleichzeitigen FP darum ging, Entscheidungen auf der Grundlage des alten Staates zu treffen.
In einem Bankenantrag würden wir niemals eine Entscheidung auf der Grundlage einer Momentaufnahme des Staates treffen wollen, die inzwischen durch eine neuere ersetzt wurde (eine Rücknahme ist erfolgt).
Diese Parallelität ist einfach, da das FP-Paradigma einen veränderlichen Zustand vermeidet. Dies ist eine technische Behauptung, die nichts über die logischen Vorzüge der Entscheidungsgrundlage für einen möglicherweise alten Zustand aussagt. FP modelliert letztendlich immer noch den Zustandswechsel. Daran führt kein Weg vorbei.
quelle
Ich wollte diese allgemeine Frage als jemand einschätzen, der ein funktionierender Neuling ist, aber über die Jahre hinweg Nebenwirkungen in meinen Augen hatte, und ich möchte sie aus allen möglichen Gründen mildern, auch aus einfacheren (oder speziell "sichereren" Gründen. weniger fehleranfällig "). Wenn ich zu meinen funktionalen Kollegen schaue und sehe, was sie tun, wirkt das Gras ein bisschen grüner und riecht zumindest in dieser Hinsicht besser.
Serielle Algorithmen
Zu Ihrem speziellen Beispiel: Wenn Ihr Problem serieller Natur ist und B erst nach Abschluss von A ausgeführt werden kann, können Sie A und B konzeptionell nicht parallel ausführen, egal was passiert. Sie müssen einen Weg finden, um die Ordnungsabhängigkeit wie in Ihrer Antwort zu unterbrechen, indem Sie parallele Züge mit dem alten Spielstatus ausführen, oder Sie müssen eine Datenstruktur verwenden, mit der Teile davon unabhängig modifiziert werden können, um die in den anderen Antworten vorgeschlagene Ordnungsabhängigkeit zu beseitigen oder so etwas. Aber es gibt definitiv einige konzeptionelle Designprobleme wie diese, bei denen man nicht unbedingt alles so einfach multithreaden kann, weil die Dinge unveränderlich sind. Einige Dinge werden nur serieller Natur sein, bis Sie einen intelligenten Weg finden, um die Auftragsabhängigkeit zu brechen, falls dies überhaupt möglich ist.
Einfachere Parallelität
In vielen Fällen gelingt es uns jedoch nicht, Programme mit Nebenwirkungen an Stellen zu parallelisieren, die möglicherweise die Leistung erheblich verbessern, weil sie möglicherweise nicht threadsicher sind. Einer der Fälle, in denen das Eliminieren des veränderlichen Zustands (oder genauer gesagt der externen Nebenwirkungen) viel hilft, wie ich sehe, ist, dass er "möglicherweise threadsicher" in "definitiv threadsicher" verwandelt oder möglicherweise nicht .
Um diese Aussage ein wenig konkreter zu machen, überlege ich, dass ich Ihnen die Aufgabe gebe, eine Sortierfunktion in C zu implementieren, die einen Komparator akzeptiert und diese verwendet, um ein Array von Elementen zu sortieren. Es ist allgemein gehalten, aber ich gehe davon aus, dass es für Eingaben mit einer solchen Größenordnung (Millionen von Elementen oder mehr) verwendet wird, dass es zweifellos von Vorteil ist, immer eine Multithread-Implementierung zu verwenden. Können Sie Ihre Sortierfunktion mit mehreren Threads ausführen?
Das Problem ist, dass Sie nicht können, weil die Komparatoren Ihre Sortierfunktion möglicherweise aufruftNebenwirkungen verursachen, es sei denn, Sie wissen, wie sie für alle möglichen Fälle implementiert (oder zumindest dokumentiert) sind, was ohne eine Degeneralisierung der Funktion unmöglich ist. Ein Komparator könnte etwas Ekelhaftes tun, wie eine globale Variable in einer nicht-atomaren Weise zu modifizieren. 99,9999% der Komparatoren tun dies möglicherweise nicht, aber wir können diese verallgemeinerte Funktion nicht einfach wegen der 0,00001% der Fälle, die Nebenwirkungen verursachen könnten, multithreaden. Infolgedessen müssen Sie möglicherweise sowohl eine Single-Thread- als auch eine Multithread-Sortierfunktion anbieten und die Verantwortung auf die Programmierer übertragen, die sie verwenden, um auf der Grundlage der Threadsicherheit zu entscheiden, welche verwendet werden soll. Und die Leute könnten immer noch die Single-Thread-Version verwenden und Möglichkeiten zum Multithreading verpassen, weil sie möglicherweise auch unsicher sind, ob der Komparator thread-sicher ist.
Es gibt eine ganze Menge Denkvermögen, das in die Rationalisierung der Thread-Sicherheit von Dingen involviert sein kann, ohne überall Sperren zu werfen, die verschwinden können, wenn wir nur harte Garantien hatten, dass Funktionen für jetzt und für die Zukunft keine Nebenwirkungen verursachen. Und es gibt Angst: praktische Angst, denn jeder, der ein paar Mal zu oft eine Race-Bedingung debuggen musste, würde wahrscheinlich zögern, Multithreading-Vorgänge durchzuführen, bei denen er nicht zu 110% sicher sein kann, dass sie thread-sicher sind und dies auch bleiben werden. Selbst für die paranoidsten (von denen ich wahrscheinlich zumindest an der Grenze bin) bietet die reine Funktion das Gefühl der Erleichterung und des Vertrauens, das wir sicher parallel nennen können.
Und das ist einer der Hauptfälle, in denen ich es als so vorteilhaft betrachte, wenn Sie eine harte Garantie dafür erhalten, dass solche Funktionen thread-sicher sind, die Sie mit reinen funktionalen Sprachen erhalten. Die andere ist, dass funktionale Sprachen oftmals die Schaffung von Funktionen fördern, die in erster Linie frei von Nebenwirkungen sind. Sie können beispielsweise persistente Datenstrukturen bereitstellen, bei denen es recht effizient ist, eine massive Datenstruktur einzugeben und dann eine brandneue auszugeben, bei der nur ein kleiner Teil der Daten vom Original abweicht, ohne das Original zu berühren. Diejenigen, die ohne solche Datenstrukturen arbeiten, möchten diese möglicherweise direkt ändern und verlieren dabei etwas Threadsicherheit.
Nebenwirkungen
Trotzdem bin ich mit einem Teil nicht einverstanden, obwohl ich meinen funktionellen Freunden gebührenden Respekt entgegenbringe (die ich für super cool halte):
Es ist nicht unbedingt Unveränderlichkeit, die Parallelität so praktisch macht, wie ich es sehe. Es sind Funktionen, die Nebenwirkungen vermeiden. Wenn eine Funktion ein Array zum Sortieren eingibt, es kopiert und dann die Kopie zum Sortieren des Inhalts mutiert und die Kopie ausgibt, ist sie genauso threadsicher wie eine Funktion, die mit einem unveränderlichen Array-Typ arbeitet, selbst wenn Sie dieselbe Eingabe übergeben Array von mehreren Threads darauf. Ich denke also, dass es immer noch einen Platz für veränderbare Typen gibt, wenn man sozusagen sehr parallelen Code erstellt, obwohl unveränderliche Typen eine Menge zusätzlicher Vorteile bieten, einschließlich beständiger Datenstrukturen, die ich nicht so sehr für ihre unveränderlichen Eigenschaften verwende, sondern für Vermeiden Sie die Kosten für das Kopieren von Inhalten, um Funktionen ohne Nebenwirkungen zu erstellen.
Und es ist oft mit einem Mehraufwand verbunden, Funktionen in Form von Mischen und Kopieren einiger zusätzlicher Daten, möglicherweise einer zusätzlichen Indirektionsebene und möglicherweise einer GC für Teile einer persistenten Datenstruktur, frei von Nebenwirkungen zu machen eine 32-Core-Maschine und ich denke, der Austausch lohnt sich wahrscheinlich, wenn wir sicherer mehr Dinge parallel tun können.
quelle