92 votes

Comment/pourquoi les langages fonctionnels (en particulier Erlang) se développent-ils bien ?

Cela fait un moment que j'observe la visibilité croissante des langages de programmation fonctionnelle et de leurs fonctionnalités. Je les ai examinés et je n'ai pas vu la raison de cet attrait.

Puis, récemment, j'ai assisté à la présentation de Kevin Smith sur les "Bases d'Erlang" à l'occasion de la conférence de la Commission européenne. Codemash .

J'ai apprécié la présentation et j'ai appris que de nombreux attributs de la programmation fonctionnelle permettent d'éviter beaucoup plus facilement les problèmes de threading/concurrence. Je comprends que l'absence d'état et de mutabilité rend impossible la modification des mêmes données par plusieurs threads, mais Kevin a dit (si j'ai bien compris) que toute la communication se fait par le biais de messages et que les messages sont traités de manière synchrone (ce qui évite à nouveau les problèmes de concurrence).

Mais j'ai lu qu'Erlang est utilisé dans des applications hautement évolutives (la raison pour laquelle Ericsson l'a créé en premier lieu). Comment peut-il être efficace de traiter des milliers de demandes par seconde si tout est traité comme un message synchrone ? N'est-ce pas la raison pour laquelle nous avons commencé à évoluer vers le traitement asynchrone, afin de pouvoir tirer parti de l'exécution de plusieurs fils d'opérations en même temps et d'atteindre l'évolutivité ? Il semble que cette architecture, bien que plus sûre, constitue un pas en arrière en termes d'évolutivité. Qu'est-ce qui m'échappe ?

Je comprends que les créateurs d'Erlang aient intentionnellement évité de prendre en charge le threading pour éviter les problèmes de concurrence, mais je pensais que le multi-threading était nécessaire pour atteindre l'évolutivité.

Comment les langages de programmation fonctionnels peuvent-ils être intrinsèquement sûrs pour les threads, tout en restant évolutifs ?

1 votes

[Non mentionné] : La VM d'Erlangs porte l'asynchronisme à un autre niveau. Par magie vaudou (asm), elle permet aux opérations de synchronisation comme socket:read de se bloquer sans arrêter un thread OS. Cela vous permet d'écrire du code synchrone alors que d'autres langages vous forceraient à utiliser des nids de callback asynchrones. Il est beaucoup plus facile d'écrire une application évolutive en ayant à l'esprit des micro-services à fil unique plutôt que de garder la vue d'ensemble à l'esprit chaque fois que vous ajoutez quelque chose à la base de code.

0 votes

@Vans S Intéressant.

100voto

Godeke Points 10401

Un langage fonctionnel ne dépend pas (en général) de en mutation une variable. De ce fait, nous n'avons pas à protéger l'"état partagé" d'une variable, car sa valeur est fixe. Cela permet d'éviter la majorité des sauts de puce que les langages traditionnels doivent effectuer pour mettre en œuvre un algorithme sur plusieurs processeurs ou machines.

Erlang va plus loin que les langages fonctionnels traditionnels en intégrant un système de passage de messages qui permet à tout de fonctionner sur un système basé sur des événements où un morceau de code ne se préoccupe que de recevoir des messages et d'en envoyer, sans se soucier d'une image plus large.

Cela signifie que le programmeur ne se soucie pas (en principe) de savoir si le message sera traité par un autre processeur ou une autre machine : le simple fait d'envoyer le message suffit pour qu'il continue. S'il se soucie d'une réponse, il l'attendra en tant que un autre message .

Le résultat final est que chaque extrait est indépendant de tous les autres extraits. Pas de code partagé, pas d'état partagé et toutes les interactions proviennent d'un système de messages qui peut être distribué entre de nombreuses pièces de matériel (ou non).

Comparez cela avec un système traditionnel : nous devons placer des mutex et des sémaphores autour des variables "protégées" et de l'exécution du code. Nous avons une liaison étroite dans un appel de fonction via la pile (en attendant que le retour se produise). Tout cela crée des goulots d'étranglement qui sont moins problématiques dans un système de rien partagé comme Erlang.

EDIT : Je dois également souligner qu'Erlang est asynchrone. Vous envoyez votre message et peut-être/mais un autre message arrive en retour. Ou pas.

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

0 votes

Je comprends cela, mais je ne vois pas en quoi le modèle de message est efficace. Je dirais même le contraire. C'est une véritable révélation pour moi. Pas étonnant que les langages de programmation fonctionnels suscitent autant d'intérêt.

3 votes

Vous gagnez beaucoup de concurrence potentiel dans un système de rien partagé. Une mauvaise implémentation (une surcharge élevée de passage de messages, par exemple) pourrait torpiller ceci, mais Erlang semble bien faire les choses et garder tout léger.

0 votes

Il est important de noter que si Erlang a une sémantique de passage de message, il a une implémentation de mémoire partagée, donc, il a la sémantique décrite mais il ne copie pas des choses partout s'il n'est pas obligé de le faire.

74voto

Spencer Ruport Points 24589

Le système de file d'attente de messages est cool parce qu'il produit effectivement un effet "tirer et attendre le résultat" qui est la partie synchrone que vous êtes en train de lire. Ce qui rend cela 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érons pour l'instant que la méthodeWithALotOfDiskProcessing() prend environ 2 secondes et que la méthodeWithALotOfNetworkProcessing() prend environ 1 seconde. Dans un langage procédural, l'exécution de ce code prendrait environ 3 secondes car les lignes seraient exécutées de manière séquentielle. Nous perdons du temps à attendre l'achèvement d'une méthode qui pourrait s'exécuter en même temps que l'autre sans se disputer une seule ressource. Dans un langage fonctionnel, les lignes de code ne dictent pas quand le processeur va les essayer. Un langage fonctionnel 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.

C'est cool, non ? En poursuivant le code et en n'attendant que là où c'est nécessaire, nous avons réduit automatiquement le temps d'attente à deux secondes ! :D Donc oui, bien que le code soit synchrone, il a tendance à avoir une signification différente de celle des langages procéduraux.

EDITAR:

Une fois que vous saisissez ce concept en conjonction avec le poste de Godeke, il est facile d'imaginer comment simple il devient possible de tirer parti de processeurs multiples, de fermes de serveurs, de magasins de données redondants et de je ne sais quoi encore.

0 votes

Cool ! J'ai totalement mal compris comment les messages étaient traités. Merci, votre message m'aide.

0 votes

"Un langage fonctionnel essaierait quelque chose comme ce qui suit" - Je ne suis pas sûr des autres langages fonctionnels, mais en Erlang l'exemple fonctionnerait exactement comme dans le cas des langages procéduraux. Vous puede exécuter ces deux tâches en parallèle en créant des processus, en les laissant exécuter les deux tâches de manière asynchrone et en obtenant leurs résultats à la fin, mais ce n'est pas comme si "si le code est synchrone, il a tendance à avoir une signification différente de celle des langages procéduraux". Voir aussi la réponse de Chris.

16voto

Chris Czura Points 141

Il est probable que vous confondiez synchrone con séquentiel .

Le corps d'une fonction en erlang est traité de manière séquentielle. Donc ce que Spencer a dit à propos de cet "effet automatique" n'est pas vrai pour erlang. Vous pourriez cependant modéliser ce comportement avec erlang.

Par exemple, vous pouvez créer un processus qui calcule le nombre de mots dans une ligne. Comme nous avons plusieurs lignes, nous créons un tel processus pour chaque ligne et recevons les réponses pour calculer une somme.

De cette façon, nous créons des processus qui effectuent les calculs "lourds" (en utilisant des cœurs supplémentaires s'ils sont disponibles) et nous collectons ensuite 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 voilà à quoi ça ressemble, quand on le lance dans le shell :

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>

13voto

Gordon Guthrie Points 4108

L'élément clé qui permet à Erlang d'évoluer est lié à la concurrence.

Un système d'exploitation fournit la concurrence par deux mécanismes :

  • les processus du système d'exploitation
  • fils du système d'exploitation

Les processus ne partagent pas d'état - un processus ne peut pas en planter un autre par conception.

Les threads partagent l'état - un thread peut planter un autre par conception - c'est votre problème.

Avec Erlang - un processus du système d'exploitation est utilisé par la machine virtuelle et la VM fournit la concurrence au programme Erlang non pas en utilisant les threads du système d'exploitation mais en fournissant des processus Erlang - c'est-à-dire qu'Erlang implémente son propre timelicer.

Ces processus Erlang communiquent entre eux en envoyant des messages (gérés par la VM Erlang et non par le système d'exploitation). Les processus Erlang s'adressent les uns aux autres en utilisant un identifiant de processus (PID) qui a une adresse en trois parties <<N3.N2.N1>> :

  • processus non N1 sur
  • VM N2 sur
  • machine physique N3

Deux processus sur la même machine virtuelle, sur des machines virtuelles différentes sur la même machine ou sur deux machines communiquent de la même manière - votre mise à l'échelle est donc indépendante du nombre de machines physiques sur lesquelles vous déployez votre application (en première approximation).

Erlang n'est threadsafe que dans un sens trivial - il n'a pas de threads. (Le langage qui l'est, la VM SMP/multi-core utilise un thread du système d'exploitation par cœur).

7voto

Kristopher Johnson Points 34554

Vous avez peut-être une mauvaise compréhension du fonctionnement d'Erlang. Le runtime Erlang minimise le changement de contexte sur un CPU, mais si plusieurs CPU sont disponibles, ils sont tous utilisés pour traiter les messages. Il n'y a pas de "threads" au sens où on l'entend dans d'autres langages, mais on peut avoir beaucoup de messages traités simultanément.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X