web-dev-qa-db-fra.com

Comment / pourquoi les langues fonctionnelles (spécifiquement erlang) sont-elles bien?

J'ai regardé la visibilité croissante des langages et des fonctionnalités de programmation fonctionnelle pendant un moment. Je les ai regardés et je n'ai pas vu la raison de l'appel.

Ensuite, j'ai récemment assisté à la présentation de Kevin Smith's of Erlang "de Kevin Smith à CODEMASH .

J'ai apprécié la présentation et j'ai appris que de nombreux attributs de la programmation fonctionnelle facilitent beaucoup plus facilement les problèmes de filetage/concurrence. Je comprends que le manque d'état et de mutabilité empêche de multiples threads de modifier les mêmes données, mais que Kevin a dit (si j'ai bien compris) toutes les communications ont lieu via des messages et les mésages sont traités de manière synchrone (à nouveau éviter les problèmes de concurrence).

Mais j'ai lu que Erlang est utilisé dans des applications hautement évolutives (la raison entière Ericsson l'a créée en premier lieu). Comment peut-être une manipulation efficace des milliers de demandes par seconde si tout est traité comme un message traité de manière synchrone? N'est-ce pas pourquoi nous avons commencé à se déplacer vers une transformation asynchrone - nous pouvons donc tirer parti de la gestion de plusieurs threads d'opération à la fois et de réaliser une évolutivité? Cela semble être cette architecture, tandis que Safer, est un pas en arrière en termes d'évolutivité. Qu'est-ce que je rate?

Je comprends que les créateurs d'Erlang ont intentionnellement évité intentionnellement le filetage pour éviter les problèmes de concurrence, mais je pensais que le multi-threading était nécessaire pour réaliser une évolutivité.

Comment les langages de programmation fonctionnelle peuvent-ils être intrinsèquement thread-coffre-fort, encore une échelle fixe?

89
Jim Anderson

Une langue fonctionnelle ne s'appuie pas (en général) sur la mutation d'une variable. Pour cette raison, nous n'avons pas besoin de protéger l'état "partagé" d'une variable, car la valeur est corrigée. Cela évite à son tour la majorité du cerceau qui saute que les langues traditionnelles doivent passer pour mettre en œuvre un algorithme entre les processeurs ou les machines.

Erlang le prend plus loin que les langages fonctionnels traditionnels en cuisson dans un système de passage de message qui permet à tout de fonctionner sur un système basé sur l'événement où un morceau de code ne soutient que de recevoir des messages et d'envoyer des messages, ne vous inquiétez pas d'une image plus grande.

Cela signifie que le programmeur est (nominalement) indifférent que le message sera traité sur un autre processeur ou une machine: il suffit de simplement envoyer le message pour qu'il continue. Si cela se soucie d'une réponse, il l'attendra comme n autre message.

Le résultat final est que chaque extrait est indépendant de tous les autres extraits. Aucun code partagé, aucun état partagé et toutes les interactions provenant d'un système de messagerie pouvant être distribuée entre de nombreux matériels (ou non).

Contrôlez cela avec un système traditionnel: nous devons placer des mutiles et des sémaphores autour des variables "protégées" et une exécution de code. Nous avons une liaison étroite dans un appel de la fonction via la pile (en attente du retour sur lequel se produire). Tout cela crée des goulots d'étranglement moins un problème dans un système de rien partagé comme Erlang.

Edit: Je devrais aussi souligner que Erlang est asynchrone. Vous envoyez votre message et peut-être/un jour un autre message arrive. Ou pas.

Le point de Spencer sur l'exécution hors de l'ordre est également important et répondu bien.

96
Godeke

Le système de file d'attente de messages est cool car il produit efficacement un effet "feu-and-wait-wait-forit" qui est la partie synchrone que vous lisez sur. Ce qui rend cet incroyablement génial, c'est que cela signifie que les lignes n'ont pas besoin d'être exécutées séquentiellement. Considérez le code suivant:

r = methodWithALotOfDiskProcessing();
x = r + 1;
y = methodWithALotOfNetworkProcessing();
w = x * y

Considérez un instant que la méthodeWITHAlotofDiskProcessing () prend environ 2 secondes à compléter et que la méthodeWITHAlotofNetworkProcessing () prend environ 1 seconde à compléter. Dans une langue de procédure, ce code prendrait environ 3 secondes pour fonctionner car les lignes seraient exécutées de manière séquentielle. Nous avons perdu du temps en attente d'une méthode pour pouvoir fonctionner simultanément avec l'autre sans participer à une seule ressource. Dans une ligne de langue fonctionnelle de code ne dicte pas lorsque le processeur les tentera. Une langue fonctionnelle essaierait quelque chose comme ce qui suit:

Execute line 1 ... wait.
Execute line 2 ... wait for r value.
Execute line 3 ... wait.
Execute line 4 ... wait for x and y value.
Line 3 returned ... y value set, message line 4.
Line 1 returned ... r value set, message line 2.
Line 2 returned ... x value set, message line 4.
Line 4 returned ... done.

À quel point cela est cool? En allant devant le code et qu'attend uniquement si nécessaire, nous avons réduit le temps d'attente à deux secondes automatiquement! : D Donc, tandis que le code est synchrone, il a tendance à avoir une signification différente de celle des langues procédurales.

ÉDITER:

Une fois que vous saisissez ce concept en conjonction avec le poteau de Godeke, il est facile d'imaginer comment simple Il devient de tirer parti de plusieurs processeurs, fermes de serveurs, magasins de données redondants et qui sait quoi d'autre.

73
Spencer Ruport

Il est probable que vous mélangez synchrones avec séquentiel.

Le corps d'une fonction dans Erlang est en cours de traitement de manière séquentielle. Alors, quels Spencer a dit à propos de cet "effet automagal" ne tient pas vrai pour Erlang. Vous pouvez modéliser ce comportement avec Erlang cependant.

Par exemple, vous pouvez apparaître un processus qui calcule le nombre de mots dans une ligne. Comme nous avons plusieurs lignes, nous apparaissons un tel processus pour chaque ligne et reçoions les réponses pour calculer une somme de celle-ci.

De cette façon, nous reproduisons les processus qui font les calculs "lourds" (utilisant des noyaux supplémentaires si disponibles) et, plus tard, nous collectons les résultats.

-module(countwords).
-export([count_words_in_lines/1]).

count_words_in_lines(Lines) ->
    % For each line in lines run spawn_summarizer with the process id (pid)
    % and a line to work on as arguments.
    % This is a list comprehension and spawn_summarizer will return the pid
    % of the process that was created. So the variable Pids will hold a list
    % of process ids.
    Pids = [spawn_summarizer(self(), Line) || Line <- Lines], 
    % For each pid receive the answer. This will happen in the same order in
    % which the processes were created, because we saved [pid1, pid2, ...] in
    % the variable Pids and now we consume this list.
    Results = [receive_result(Pid) || Pid <- Pids],
    % Sum up the results.
    WordCount = lists:sum(Results),
    io:format("We've got ~p words, Sir!~n", [WordCount]).

spawn_summarizer(S, Line) ->
    % Create a anonymous function and save it in the variable F.
    F = fun() ->
        % Split line into words.
        ListOfWords = string:tokens(Line, " "),
        Length = length(ListOfWords),
        io:format("process ~p calculated ~p words~n", [self(), Length]),
        % Send a Tuple containing our pid and Length to S.
        S ! {self(), Length}
    end,
    % There is no return in erlang, instead the last value in a function is
    % returned implicitly.
    % Spawn the anonymous function and return the pid of the new process.
    spawn(F).

% The Variable Pid gets bound in the function head.
% In erlang, you can only assign to a variable once.
receive_result(Pid) ->
    receive
        % Pattern-matching: the block behind "->" will execute only if we receive
        % a Tuple that matches the one below. The variable Pid is already bound,
        % so we are waiting here for the answer of a specific process.
        % N is unbound so we accept any value.
        {Pid, N} ->
            io:format("Received \"~p\" from process ~p~n", [N, Pid]),
            N
    end.

Et c'est à quoi cela ressemble, lorsque nous courons cela dans la coquille:

Eshell V5.6.5  (abort with ^G)
1> Lines = ["This is a string of text", "and this is another", "and yet another", "it's getting boring now"].
["This is a string of text","and this is another",
 "and yet another","it's getting boring now"]
2> c(countwords).
{ok,countwords}
3> countwords:count_words_in_lines(Lines).
process <0.39.0> calculated 6 words
process <0.40.0> calculated 4 words
process <0.41.0> calculated 3 words
process <0.42.0> calculated 4 words
Received "6" from process <0.39.0>
Received "4" from process <0.40.0>
Received "3" from process <0.41.0>
Received "4" from process <0.42.0>
We've got 17 words, Sir!
ok
4> 
16
Chris Czura

La chose clé qui permet à Erlang à l'échelle est liée à la concurrence.

Un système d'exploitation offre une concurrence de deux mécanismes:

  • processus du système d'exploitation
  • threads du système d'exploitation

Les processus ne partagent pas l'état - Un processus ne peut pas en collier une autre par conception.

Threads Share State - Un thread peut s'écraser une autre par conception - c'est votre problème.

Avec Erlang - un processus de système d'exploitation est utilisé par la machine virtuelle et le VM fournit au programme Erlang concurrency non pas en utilisant des fils du système d'exploitation, mais en fournissant des processus Erlang - qui Erlang implémente sa propre timeslicer.

Ces processus Erlang se parlent en envoyant des messages (traités par le erlang VM pas le système d'exploitation). Les processus Erlang s'adressent à l'aide d'un ID de processus (PID) qui a une troisième partie. adresse <<N3.N2.N1>>:

  • processus no n1 sur
  • Vm n2 sur
  • machine physique n3

Deux procédés sur le même VM, sur différents VM sur la même machine ou deux machines communiquent de la même manière - votre échelle est donc indépendante du nombre de machines physiques que vous déployez votre application sur (dans la première approximation).

Erlang n'est que Threadsafe dans un sens trivial - il n'a pas de filetage. (La langue qui est, le SMP/Multi-Core VM utilise un fil de système d'exploitation par noyau).

13
Gordon Guthrie

Vous pouvez avoir un malentendu de la façon dont erlang fonctionne. Le temps d'exécution Erlang minimise la commutation de contexte sur une CPU, mais s'il y a plusieurs processeurs disponibles, tous sont utilisés pour traiter les messages. Vous n'avez pas de "threads" dans le sens où vous faites dans d'autres langues, mais vous pouvez avoir beaucoup de messages en cours de traitement simultanément.

7
Kristopher Johnson

Les messages Erlang sont purement asynchrones, si vous souhaitez une réponse synchrone à votre message, vous devez explicitement coder pour cela. Ce qui a été éventuellement dit était que les messages dans une zone de message de processus sont traités de manière séquentielle. Tout message envoyé à un processus est assis dans la zone de message de processus et le processus permet de choisir un message à partir de ce processus de la boîte, puis passez à la suivante, dans l'ordre qu'il voit en forme. Ceci est un acte très séquentiel et le bloc de réception fait exactement cela.

On dirait que vous avez mélangé synchrone et séquentiel comme Chris mentionné.

4
Jebu
3
Kristopher Johnson