it-swarm.com.de

Was macht die funktionale Programmierung von Natur aus für die parallele Ausführung geeignet?

Ich habe immer wieder gelesen, dass funktionale Sprachen ideal (oder zumindest sehr oft nützlich) für Parallelität sind. Warum ist das? Welche Kernkonzepte und Paradigmen werden typischerweise verwendet und welche spezifischen Probleme lösen sie?

Auf einer abstrakten Ebene kann ich zum Beispiel sehen, wie unveränderlich es sein kann, Rennbedingungen oder andere Probleme, die sich aus dem Ressourcenwettbewerb ergeben, zu verhindern, aber ich kann es nicht genauer formulieren. Bitte beachten Sie jedoch, dass meine Frage einen breiteren Anwendungsbereich hat als nur Unveränderlichkeit - eine gute Antwort liefert Beispiele für mehrere relevante Konzepte.

44
blz

Der Hauptgrund ist, dass referenzielle Transparenz (und noch mehr Faulheit) über die Ausführungsreihenfolge abstrahiert. Dies macht es trivial, die Bewertung zu parallelisieren.

Wenn beispielsweise sowohl a, b als auch || referenziell transparent sind, spielt es keine Rolle, ob in

a || b

a wird zuerst ausgewertet, b wird zuerst ausgewertet oder b wird überhaupt nicht ausgewertet (weil a zu true ausgewertet wurde).

Im

a || a

es spielt keine Rolle, ob a ein- oder zweimal ausgewertet wird (oder sogar fünfmal… was keinen Sinn ergibt, aber trotzdem keine Rolle spielt).

Wenn es also nicht darauf ankommt, in welcher Reihenfolge sie ausgewertet werden und ob sie unnötig ausgewertet werden, können Sie einfach jeden Unterausdruck parallel auswerten. Wir könnten also a und b parallel auswerten und dann || warten, bis einer der beiden Threads beendet ist, schauen, was er zurückgegeben hat, und wenn er true, es könnte sogar den anderen abbrechen und sofort true zurückgeben.

Jeder Unterausdruck kann parallel ausgewertet werden. Trivial.

Beachten Sie jedoch, dass dies kein Wundermittel ist. Einige experimentelle frühe Versionen von GHC haben dies getan, und es war eine Katastrophe: Es gab nur zu viel mögliche Parallelität. Selbst ein einfaches Programm kann Hunderte, Tausende, Millionen von Threads erzeugen, und für die überwiegende Mehrheit der Subausdrücke dauert das Laichen des Threads viel länger als nur die Auswertung des Ausdrucks. Bei so vielen Threads dominiert die Kontextwechselzeit jede nützliche Berechnung vollständig.

Man könnte sagen, dass die funktionale Programmierung das Problem auf den Kopf stellt: In der Regel besteht das Problem darin, ein serielles Programm in genau die richtige Größe paralleler "Chunks" zu zerlegen, während bei der funktionalen Programmierung das Problem darin besteht, parallele Sub-Gruppen zu gruppieren -Programme in serielle "Chunks".

GHC macht es heute so, dass Sie zwei parallel auszuwertende Unterausdrücke manuell mit Anmerkungen versehen können. Dies ähnelt tatsächlich der Vorgehensweise in einer imperativen Sprache, indem Sie die beiden Ausdrücke in separate Threads einfügen. Es gibt jedoch einen wichtigen Unterschied: Das Hinzufügen dieser Anmerkung kann das Ergebnis des Programms niemals ändern! Es kann es schneller machen, es kann es langsamer machen, es kann mehr Speicher verbrauchen, aber es kann nicht sein Ergebnis ändern. Dies macht es way einfacher, mit Parallelität zu experimentieren, um genau das richtige Maß an Parallelität und die richtige Größe von Chunks zu finden.

68
Jörg W Mittag

Schauen wir uns zunächst an, warum die prozedurale Programmierung bei gleichzeitigen Threads so schlecht ist.

Bei einem gleichzeitigen Programmiermodell schreiben Sie sequentielle Anweisungen, die (standardmäßig) voraussichtlich isoliert ausgeführt werden. Wenn Sie mehrere Threads einführen, müssen Sie den Zugriff explizit steuern, um den gleichzeitigen Zugriff auf eine gemeinsam genutzte Variable zu verhindern, wenn sich diese Änderungen gegenseitig beeinflussen können. Es ist schwierig, diese Programmierung richtig zu machen, und beim Testen ist es unmöglich zu beweisen, dass sie sicher durchgeführt wurde. Bestenfalls können Sie nur bestätigen, dass bei diesem Testlauf keine beobachtbaren Probleme aufgetreten sind.

Bei der funktionalen Programmierung ist das Problem anders. Es gibt keine gemeinsam genutzten Daten. Es gibt keinen gleichzeitigen Zugriff auf dieselbe Variable. In der Tat können Sie eine Variable nur einmal setzen, es gibt keine "for-Schleifen", es gibt nur Codeblöcke, die bei Ausführung mit einem bestimmten Wertesatz immer das gleiche Ergebnis erzielen. Dies macht das Testen sowohl vorhersehbar als auch ein guter Indikator für die Richtigkeit.

Das Problem, das der Entwickler bei der funktionalen Programmierung lösen muss, besteht darin, die Lösung so zu gestalten, dass der allgemeine Zustand minimiert wird. Wenn das Entwurfsproblem ein hohes Maß an Parallelität und einen minimalen gemeinsamen Status erfordert, sind funktionale Implementierungen eine sehr effektive Strategie.

4
Michael Shaw

Minimierter gemeinsamer Status

Was macht die funktionale Programmierung von Natur aus für die parallele Ausführung geeignet?

Die reine Natur der Funktionen ( referenzielle Transparenz ), d. H. Ohne Nebenwirkungen, führt zu weniger gemeinsam genutzten Objekten und damit zu einem weniger gemeinsam genutzten Zustand.

Ein einfaches Beispiel ist;

double CircleCircumference(double radius)
{
  return 2 * 3.14 * radius; // constants for illustration
}

Die Ausgabe ist ausschließlich von der Eingabe abhängig. Es ist kein Status enthalten. Im Gegensatz zu einer Funktion wie GetNextStep();, bei der die Ausgabe von dem aktuellen Schritt abhängt (normalerweise als Datenelement eines Objekts gehalten).

Es ist der gemeinsam genutzte Status, für den der Zugriff über Mutexe und Sperren gesteuert werden muss. Stärkere Garantien für den gemeinsamen Zustand ermöglichen bessere parallele Optimierungen und eine bessere parallele Zusammensetzung.


Referenzielle Transparenz und reine Ausdrücke

Weitere Einzelheiten zur referenziellen Transparenz finden Sie hier auf Programmers.SE .

[Es] bedeutet, dass Sie jeden Ausdruck im Programm durch das Ergebnis der Auswertung dieses Ausdrucks ersetzen können (oder umgekehrt), ohne die Bedeutung des Programms zu ändern. Jörg W Mittag

Was wiederum reine Ausdrücke erlaubt, die verwendet werden, um reine Funktionen zu konstruieren - Funktionen, die bei gleichen Eingabeargumenten das gleiche Ergebnis erzielen. Jeder Ausdruck kann somit parallel ausgewertet werden.

3
Niall

Der schwierigste Teil beim Schreiben von parallelem Code besteht darin, zu verhindern, dass ein Thread Daten liest, die von einem anderen Thread aktualisiert werden.

Eine übliche Lösung hierfür ist die Verwendung von unveränderlichen Objekten , sodass ein einmal erstelltes Objekt niemals aktualisiert wird. Im wirklichen Leben müssen Daten jedoch geändert werden, daher wird "Persistenz" -Daten verwendet, wobei jedes Update ein neues Objekt zurückgibt - dies kann durch sorgfältige Auswahl der Datenstrukturen effizient gemacht werden.

Da die funktionale Programmierung keine Nebenwirkungen zulässt, werden bei der funktionalen Programmierung normalerweise „Persistenzobjekte“ verwendet. Der Schmerz, den ein funktionierender Programmierer aufgrund fehlender Nebenwirkungen erleiden muss, führt zu einer Lösung, die für die parallele Programmierung gut funktioniert.

Ein weiterer Vorteil besteht darin, dass funktionale Sprachsysteme überprüfen, ob Sie die Regeln einhalten, und über zahlreiche Tools verfügen, die Ihnen dabei helfen.

1
Ian

Reiner Funktionscode ist standardmäßig threadsicher.

Das allein ist schon ein großer Gewinn. In anderen Programmiersprachen kann das Entwerfen von Codeblöcken, die vollständig threadsicher sind, eine echte Herausforderung sein. Aber eine reine funktionale Sprache zwingt Sie zu machen alles threadsicher, außer an den wenigen Stellen, an denen Sie explizit etwas tun, das nicht threadsicher ist. Man könnte sagen, dass im imperativen Code die Thread-Sicherheit explizit ist [und daher normalerweise selten ist], während im reinen Funktionscode Thread-unsicher explizit ist [und daher normalerweise selten ist].

In einer zwingenden Sprache besteht das größte Problem darin, genügend Sperren hinzuzufügen, um seltsame Datenrennen zu verhindern, aber nicht zu viel, um die Leistung zu beeinträchtigen oder zufällige Deadlocks zu provozieren. In einer reinen funktionalen Sprache sind solche Überlegungen meistens umstritten. Das Problem ist jetzt "nur" herauszufinden, wie man die Arbeit gleichmäßig aufteilt.

In einem Extremfall haben Sie eine winzige Anzahl paralleler Aufgaben, bei denen nicht alle verfügbaren Kerne verwendet werden. Im anderen Extremfall haben Sie Milliarden winziger Aufgaben, die so viel Aufwand verursachen, dass alles langsamer wird. Natürlich ist es oft sehr schwierig herauszufinden, wie viel Arbeit ein bestimmter Funktionsaufruf leistet.

Alle diese Probleme existieren auch im imperativen Code; Es ist nur so, dass die imperativen Programmierer normalerweise zu beschäftigt sind, mit dem Versuch zu ringen, das Ding zum Laufen zu bringen überhaupt, um sich über eine feine Feinabstimmung der Aufgabengrößen Gedanken zu machen.

1