92 Stimmen

Wie / warum skaliert funktionale Sprachen (insbesondere Erlang) gut?

Ich beobachte seit geraumer Zeit die wachsende Sichtbarkeit funktionaler Programmiersprachen und -funktionen. Ich habe sie mir angesehen und sehe den Grund für ihre Anziehungskraft nicht.

Vor Kurzem habe ich jedoch Kevin Smiths Präsentation "Grundlagen von Erlang" auf Codemash besucht.

Ich habe die Präsentation genossen und erfahren, dass viele der Eigenschaften funktionaler Programmierung es erleichtern, Thread-/Concurrency-Probleme zu vermeiden. Mir ist bekannt, dass das Fehlen von Zustand und Veränderbarkeit es unmöglich macht, dass mehrere Threads dieselben Daten ändern, aber Kevin sagte (wenn ich es richtig verstanden habe), dass alle Kommunikation über Nachrichten erfolgt und die Nachrichten synchron verarbeitet werden (auch hier werden Konflikte vermieden).

Aber ich habe gelesen, dass Erlang in hoch skalierbaren Anwendungen verwendet wird (der Hauptgrund, warum Ericsson es überhaupt erst erstellt hat). Wie kann es effizient sein, Tausende von Anfragen pro Sekunde zu verarbeiten, wenn alles als synchron verarbeitete Nachricht behandelt wird? Ist das nicht der Grund, warum wir begonnen haben, uns in Richtung asynchroner Verarbeitung zu bewegen - um den Vorteil zu nutzen, mehrere Threads gleichzeitig ausführen zu können und Skalierbarkeit zu erzielen? Es scheint, als ob diese Architektur, obwohl sicherer, einen Schritt rückwärts in Bezug auf Skalierbarkeit darstellt. Was übersehe ich?

Ich verstehe, dass die Schöpfer von Erlang bewusst vermieden haben, Multithreading zu unterstützen, um Konflikte zu vermeiden, aber ich dachte, dass Multithreading notwendig sei, um Skalierbarkeit zu erreichen.

Wie können funktionale Programmiersprachen von Natur aus threadsicher sein und dennoch skalieren?

1 Stimmen

[Not mentioned]: Erlangs' VM bringt Asynchronität auf eine andere Ebene. Durch Voodoo-Magie (asm) ermöglicht es synchrone Operationen wie socket:read zu blockieren, ohne einen Betriebssystem-Thread anzuhalten. Dies ermöglicht es Ihnen, synchronen Code zu schreiben, wenn andere Sprachen Sie in async-callback-Verschachtelungen zwingen würden. Es ist viel einfacher, eine skalierbare App mit dem Bild eines single-threaded Microservices im Kopf zu schreiben, anstatt jedes Mal das große Ganze im Blick zu behalten, wenn Sie etwas zum Code hinzufügen.

0 Stimmen

@Vans S Interessant.

101voto

Godeke Punkte 15741

Eine funktionale Sprache verlässt sich im Allgemeinen nicht darauf, eine Variable zu mutieren. Aufgrund dessen müssen wir den "gemeinsamen Zustand" einer Variable nicht schützen, da der Wert fest ist. Dies vermeidet im Gegenzug den Großteil des Aufwandes, den traditionelle Sprachen durchlaufen müssen, um einen Algorithmus über Prozessoren oder Maschinen zu implementieren.

Erlang geht weiter als traditionelle funktionale Sprachen, indem es ein Nachrichtensystem integriert, das es ermöglicht, alles in einem ereignisbasierten System zu betreiben, bei dem ein Code-Stück sich nur um den Empfang und das Versenden von Nachrichten kümmert, ohne sich um ein größeres Bild sorgen zu müssen.

Was das bedeutet, ist dass der Programmierer (nominell) nicht besorgt sein muss, dass die Nachricht auf einem anderen Prozessor oder einer anderen Maschine verarbeitet wird: einfach das Senden der Nachricht reicht aus, damit sie fortgesetzt wird. Wenn es um eine Antwort kümmert, wird es darauf warten, wie eine andere Nachricht.

Das Endergebnis davon ist, dass jeder Abschnitt unabhängig von jedem anderen Abschnitt ist. Kein gemeinsamer Code, kein gemeinsamer Zustand und alle Interaktionen kommen aus einem Nachrichtensystem, das auf viele Hardwarekomponenten verteilt werden kann (oder auch nicht).

Vergleichen wir das mit einem traditionellen System: Wir müssen Mutexes und Semaphoren um "geschützte" Variablen und Codeausführung platzieren. Wir haben eine enge Bindung in einem Funktionsaufruf über den Stack (warten auf das Eintreffen des Rückgabewerts). Das alles schafft Engpässe, die in einem "Shared-Nothing"-System wie Erlang weniger problematisch sind.

EDIT: Ich sollte auch darauf hinweisen, dass Erlang asynchron ist. Du sendest deine Nachricht und vielleicht/iirgendwann kommt eine andere Nachricht zurück. Oder auch nicht.

Spencers Hinweis zu der ausgeführten Reihenfolge ist ebenfalls wichtig und gut beantwortet.

0 Stimmen

Ich verstehe das, sehe aber nicht, wie das Nachrichtenmodell effizient ist. Ich würde das Gegenteil vermuten. Das ist für mich eine echte Erkenntnis. Kein Wunder, dass funktionale Programmiersprachen so viel Aufmerksamkeit bekommen.

3 Stimmen

Du erhältst viel Konkurrenz Potenzial in einem Shared-Nothing-System. Eine schlechte Implementierung (hoher Nachrichtenaustausch zum Beispiel) könnte dies zunichte machen, aber Erlang scheint es richtig zu machen und alles leichtgewichtig zu halten.

0 Stimmen

Es ist wichtig zu beachten, dass Erlang zwar eine Nachrichtenübermittlungssemantik aufweist, but es eine gemeinsame Speicherimplementierung hat. Daher hat es die beschriebenen Semantiken, aber es kopiert nicht alles herum, wenn es nicht muss.

74voto

Spencer Ruport Punkte 34547

Das Nachrichtenwarteschlangensystem ist cool, weil es effektiv einen "Feuer-und-Warten-auf-Ergebnis"-Effekt erzeugt, der Teil synchron ist, den Sie gerade lesen. Was dies unglaublich großartig macht, ist, dass dadurch die Zeilen nicht sequenziell ausgeführt werden müssen. Betrachten Sie den folgenden Code:

r = MethodeMitVielFestplattenverarbeitung();
x = r + 1;
y = MethodeMitVielNetzwerkverarbeitung();
w = x * y

Denken Sie einen Moment darüber nach, dass MethodeMitVielFestplattenverarbeitung() etwa 2 Sekunden und MethodeMitVielNetzwerkverarbeitung() etwa 1 Sekunde dauern. In einer prozeduralen Sprache würde dieser Code etwa 3 Sekunden benötigen, um ausgeführt zu werden, weil die Zeilen sequenziell ausgeführt würden. Wir verbringen Zeit damit, auf den Abschluss einer Methode zu warten, die parallel zur anderen ohne Wettbewerb um eine einzelne Ressource ausgeführt werden könnte. In einer funktionalen Sprache geben Zeilen von Code nicht vor, wann der Prozessor sie versuchen wird. Eine funktionale Sprache würde etwas Ähnliches versuchen wie das Folgende:

Führe Zeile 1 aus ... warte.
Führe Zeile 2 aus ... warte auf den Wert r.
Führe Zeile 3 aus ... warte.
Führe Zeile 4 aus ... warte auf die Werte x und y.
Zeile 3 zurückgegeben ... Wert y gesetzt, Nachricht Zeile 4.
Zeile 1 zurückgegeben ... Wert r gesetzt, Nachricht Zeile 2.
Zeile 2 zurückgegeben ... Wert x gesetzt, Nachricht Zeile 4.
Zeile 4 zurückgegeben ... fertig.

Wie cool ist das? Indem wir mit dem Code voranschreiten und nur dort warten, wo es notwendig ist, haben wir die Wartezeit automatisch auf zwei Sekunden reduziert! :D Ja, während der Code synchron ist, hat er tendenziell eine andere Bedeutung als in prozeduralen Sprachen.

EDIT:

Wenn Sie dieses Konzept in Verbindung mit Godekes Beitrag verstehen, ist es einfach vorstellbar, wie einfach es wird, von mehreren Prozessoren, Serverfarmen, redundanten Datenspeichern und wer weiß was anderem zu profitieren.

0 Stimmen

Cool! Ich habe völlig missverstanden, wie Nachrichten behandelt werden. Danke, dein Beitrag hilft.

0 Stimmen

" Eine funktionale Sprache würde etwas Ähnliches versuchen. Ich bin mir nicht sicher, was andere funktionale Sprachen betrifft, aber in Erlang würde das Beispiel genau wie bei prozeduralen Sprachen funktionieren. Du kannst diese beiden Aufgaben parallel ausführen, indem du Prozesse spawnst, sie die beiden Aufgaben asynchron ausführen lässt und am Ende ihre Ergebnisse erhältst, aber es ist nicht so, dass "während der Code synchron ist, er eine andere Bedeutung als in prozeduralen Sprachen haben würde". Siehe auch die Antwort von Chris. "

16voto

Chris Czura Punkte 161

Es ist wahrscheinlich, dass Sie synchron mit sequenziell verwechseln.

Der Körper einer Funktion in Erlang wird sequenziell verarbeitet. Also stimmt das, was Spencer über diesen "automagischen Effekt" gesagt hat, nicht für Erlang. Sie könnten dieses Verhalten jedoch mit Erlang modellieren.

Zum Beispiel könnten Sie einen Prozess spawnen, der die Anzahl der Wörter in einer Zeile berechnet. Da wir mehrere Zeilen haben, spawnen wir einen solchen Prozess für jede Zeile und erhalten die Antworten, um eine Summe daraus zu berechnen.

Auf diese Weise spawnen wir Prozesse, die die "schweren" Berechnungen durchführen (unter Verwendung zusätzlicher Kerne, wenn verfügbar) und sammeln später die Ergebnisse.

-Modul(countwords).
-Export([count_words_in_lines/1]).

count_words_in_lines(Lines) ->
    % Für jede Zeile in den Zeilen führen wir spawn_summarizer mit der Prozess-ID (pid) aus
    % und einer Zeile zum Bearbeiten als Argumente.
    % Dies ist eine Listenabstraktion und spawn_summarizer gibt die pid zurück
    % des erstellten Prozesses. Also wird die Variable Pids eine Liste enthalten
    % von Prozess-IDs.
    Pids = [spawn_summarizer(self(), Line) || Line <- Lines], 
    % Für jede pid empfangen wir die Antwort. Dies wird in der gleichen Reihenfolge geschehen
    % in der die Prozesse erstellt wurden, weil wir [pid1, pid2, ...] in
    % der Variable Pids gespeichert haben und jetzt diese Liste verarbeiten.
    Results = [receive_result(Pid) || Pid <- Pids],
    % Addiere die Ergebnisse.
    Wortanzahl = lists:sum(Results),
    io:format("Wir haben ~p Wörter, Sir!~n", [Wortanzahl]).

spawn_summarizer(S, Line) ->
    % Erstelle eine anonyme Funktion und speichere sie in der Variablen F.
    F = fun() ->
        % Teile die Zeile in Wörter auf.
        ListeDerWörter = string:tokens(Line, " "),
        Länge = length(ListeDerWörter),
        io:format("Prozess ~p hat ~p Wörter berechnet~n", [self(), Länge]),
        % Sende ein Tupel mit unserer pid und Länge an S.
        S ! {self(), Länge}
    end,
    % In erlang gibt es kein return, stattdessen wird der letzte Wert in einer Funktion
    % implizit zurückgegeben.
    % Spawnen Sie die anonyme Funktion und geben Sie die pid des neuen Prozesses zurück.
    spawn(F).

% Die Variable Pid wird im Funktionskopf gebunden.
% In Erlang können Sie einer Variablen nur einmal einen Wert zuweisen.
receive_result(Pid) ->
    receive
        % Pattern-Matching: Der Block hinter "->" wird nur ausgeführt, wenn wir erhalten
        % ein Tupel, das zu dem untenstehenden passt. Die Variable Pid ist bereits gebunden,
        % also warten wir hier auf die Antwort eines bestimmten Prozesses.
        % N ist nicht gebunden, also akzeptieren wir jeden Wert.
        {Pid, N} ->
            io:format("Erhalten \"~p\" von Prozess ~p~n", [N, Pid]),
            N
    end.

Und so sieht es aus, wenn wir dies in der Shell ausführen:

Eshell V5.6.5  (mit ^G abbrechen)
1> Zeilen = ["Dies ist ein Text", "und das ist ein anderer", "und noch ein anderer", "es wird jetzt langweilig"].
["Dies ist ein Text","und das ist ein anderer",
 "und noch ein anderer","es wird jetzt langweilig"]
2> c(countwords).
{ok,countwords}
3> countwords:count_words_in_lines(Zeilen).
Prozess <0.39.0> hat 6 Wörter berechnet
Prozess <0.40.0> hat 4 Wörter berechnet
Prozess <0.41.0> hat 3 Wörter berechnet
Prozess <0.42.0> hat 4 Wörter berechnet
Erhalten "6" von Prozess <0.39.0>
Erhalten "4" von Prozess <0.40.0>
Erhalten "3" von Prozess <0.41.0>
Erhalten "4" von Prozess <0.42.0>
Wir haben 17 Wörter, Sir!
ok
4>

13voto

Gordon Guthrie Punkte 6172

Der Schlüssel, der es Erlang ermöglicht zu skalieren, hängt mit Nebenläufigkeit zusammen.

Ein Betriebssystem stellt Nebenläufigkeit durch zwei Mechanismen bereit:

  • Betriebssystem-Prozesse
  • Betriebssystem-Threads

Prozesse teilen keinen Zustand – ein Prozess kann per Design einen anderen nicht abstürzen lassen.

Threads teilen Zustand – ein Thread kann einen anderen per Design abstürzen lassen – das ist dein Problem.

Bei Erlang wird ein Betriebssystemprozess von der virtuellen Maschine verwendet, und die VM stellt Nebenläufigkeit für das Erlang-Programm nicht durch die Verwendung von Betriebssystemthreads, sondern durch Bereitstellung von Erlang-Prozessen bereit – das bedeutet, Erlang implementiert seinen eigenen Zeitscheider.

Diese Erlang-Prozesse kommunizieren miteinander, indem sie Nachrichten senden (vom Erlang-VM und nicht vom Betriebssystem behandelt). Die Erlang-Prozesse adressieren sich gegenseitig anhand einer Prozess-ID (PID), die eine dreiteilige Adresse <> hat:

  • Prozess Nr. N1 auf
  • VM N2 auf
  • physischer Maschine N3

Zwei Prozesse auf derselben VM, auf verschiedenen VMs derselben Maschine oder auf zwei verschiedenen Maschinen kommunizieren auf dieselbe Weise – deine Skalierung ist daher unabhängig von der Anzahl der physischen Maschinen, auf denen du deine Anwendung einsetzt (in erster Näherung).

Erlang ist nur in einem trivialen Sinne threadsicher – es hat keine Threads. (Die Sprache, die verwendet wird, die SMP/Multi-Core-VM verwendet einen Betriebssystemthread pro Kern).

7voto

Kristopher Johnson Punkte 78933

Sie haben möglicherweise ein Missverständnis darüber, wie Erlang funktioniert. Die Erlang-Laufzeit minimiert den Kontextwechsel auf einer CPU, aber wenn mehrere CPUs verfügbar sind, werden alle zum Verarbeiten von Nachrichten verwendet. Sie haben keine "Threads" im herkömmlichen Sinn wie in anderen Sprachen, aber es können gleichzeitig viele Nachrichten verarbeitet werden.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X