2009-01-23 11 views
82

He estado observando la creciente visibilidad de los lenguajes de programación y funciones funcionales por un tiempo. Los miré y no vi el motivo de la apelación.¿Cómo/por qué los lenguajes funcionales (específicamente Erlang) escalan bien?

Luego, recientemente asistí a la presentación de Kevin Smith "Conceptos básicos de Erlang" al Codemash.

Disfruté la presentación y aprendí que muchos de los atributos de la programación funcional hacen que sea mucho más fácil evitar problemas de subprocesamiento/concurrencia. Entiendo que la falta de estado y mutabilidad hace que sea imposible que múltiples hilos modifiquen los mismos datos, pero Kevin dijo (si entendí correctamente) que toda la comunicación se realiza a través de mensajes y que los mensajes se procesan de forma síncrona (evitando problemas de concurrencia).

Pero he leído que Erlang se usa en aplicaciones altamente escalables (la razón por la cual Ericsson lo creó en primer lugar). ¿Cómo puede ser eficiente manejar miles de solicitudes por segundo si todo se maneja como un mensaje procesado sincrónicamente? ¿No es por eso que comenzamos a avanzar hacia el procesamiento asíncrono, por lo que podemos aprovechar la posibilidad de ejecutar varios hilos de operación al mismo tiempo y lograr la escalabilidad? Parece que esta arquitectura, aunque más segura, es un paso atrás en términos de escalabilidad. ¿Qué me estoy perdiendo?

Entiendo que los creadores de Erlang evitaron intencionadamente el uso de subprocesos para evitar problemas de simultaneidad, pero pensé que era necesario realizar varios subprocesos para lograr la escalabilidad.

¿Cómo pueden los lenguajes de programación funcionales ser intrínsecamente seguros para subprocesos, y aún así escalarse?

+0

[No mencionado]: La máquina virtual de Erlangs lleva la asincronía a otro nivel. Por vudú magic (asm) permite operaciones de sincronización como socket: leer para bloquear sin detener un hilo de os. Esto le permite escribir código sincrónico cuando otros idiomas lo forzarían a ingresar en nidos de devolución de llamada asíncrona. Es mucho más fácil escribir una aplicación de escalado con la imagen mental de microservicios de un solo hilo, VS teniendo en cuenta el panorama general cada vez que insertas algo en la base de códigos. –

+0

@Vans S Interesante. –

Respuesta

89

Un lenguaje funcional no se basa (en general) en la mutación de una variable. Debido a esto, no tenemos que proteger el "estado compartido" de una variable, porque el valor es fijo. Esto, a su vez, evita la mayoría de los saltos en círculo que tienen que pasar los lenguajes tradicionales para implementar un algoritmo entre procesadores o máquinas.

Erlang lo lleva más allá de los lenguajes funcionales tradicionales almacenando en un sistema de paso de mensajes que permite que todo funcione en un sistema basado en eventos donde un código solo se preocupa de recibir mensajes y enviar mensajes, sin preocuparse por una imagen más grande.

Lo que esto significa es que el programador es (nominalmente) indiferente que el mensaje se manejará en otro procesador o máquina: simplemente el envío del mensaje es lo suficientemente bueno para que continúe. Si le importa una respuesta, la esperará como otro mensaje.

El resultado final de esto es que cada fragmento es independiente de cualquier otro fragmento. Sin código compartido, sin estado compartido y todas las interacciones provenientes de un sistema de mensajes que se puede distribuir entre muchas piezas de hardware (o no).

Contraste esto con un sistema tradicional: tenemos que colocar mutexes y semáforos alrededor de las variables "protegidas" y la ejecución del código. Tenemos una vinculación estrecha en una llamada de función a través de la pila (esperando que se produzca el retorno). Todo esto crea cuellos de botella que son un problema menor en un sistema de nada compartido como Erlang.

EDITAR: También debo señalar que Erlang es asincrónico. Envías tu mensaje y tal vez/algún día llegue otro mensaje. O no.

El punto de Spencer sobre la ejecución fuera de servicio también es importante y está bien respondido.

+0

Entiendo esto, pero no veo cómo el modelo de mensaje es eficiente.Yo diría lo contrario. Esto es una verdadera revelación para mí. No es de extrañar que los lenguajes de programación funcionales reciban tanta atención. –

+3

Obtiene mucha concurrencia * potencial * en un sistema de nada compartido. Una mala implementación (alto nivel de mensaje que pasa por encima, por ejemplo) podría torpedear esto, pero Erlang parece hacerlo bien y mantener todo ligero. – Godeke

+0

Es importante tener en cuenta que, si bien Erlang tiene semántica para pasar mensajes, tiene una implementación de memoria compartida, por lo tanto, tiene la semántica descrita, pero no copia nada por todos lados si no es necesario. –

6

Es posible que tenga una mala comprensión de cómo funciona Erlang. El tiempo de ejecución de Erlang minimiza el cambio de contexto en una CPU, pero si hay varias CPU disponibles, todas se utilizan para procesar mensajes. No tiene "hilos" en el sentido en que lo hace en otros idiomas, pero puede tener muchos mensajes procesados ​​al mismo tiempo.

67

El sistema de cola de mensajes es genial porque produce efectivamente un efecto de "fuego y espera por resultado" que es la parte síncrona sobre la que está leyendo. Lo que hace que esto sea increíblemente increíble es que significa que las líneas no necesitan ser ejecutadas secuencialmente. Considere el siguiente código:

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

Considérese por un momento que methodWithALotOfDiskProcessing() toma unos 2 segundos para completar y que methodWithALotOfNetworkProcessing() toma aproximadamente 1 segundo para completar. En un lenguaje de procedimiento, este código tardaría unos 3 segundos en ejecutarse porque las líneas se ejecutarían secuencialmente. Estamos perdiendo el tiempo esperando que se complete un método que podría ejecutarse simultáneamente con el otro sin competir por un solo recurso. En un lenguaje funcional, las líneas de código no dictan cuándo el procesador las intentará. Un lenguaje funcional intentaría algo como lo siguiente:

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. 

¿Qué tan bueno es eso? Al seguir adelante con el código y esperar solo donde sea necesario, hemos reducido el tiempo de espera a dos segundos automágicamente. : D Entonces, sí, aunque el código es sincrónico, tiende a tener un significado diferente al de los lenguajes de procedimiento.

EDIT:

Una vez que entender este concepto en conjunto con el post de Godeke es fácil imaginar cómo sencilla se vuelve a aprovechar múltiples procesadores, granjas de servidores, almacenamiento de datos redundantes y quién sabe qué más.

+0

¡Genial! Entiendo totalmente mal cómo se manejaban los mensajes. Gracias, tu publicación ayuda. –

+1

¡Gran explicación! –

+0

"Un lenguaje funcional intentaría algo como lo siguiente" - No estoy seguro acerca de otros lenguajes funcionales, pero en Erlang el ejemplo funcionaría exactamente como en el caso de los lenguajes de procedimiento. Usted * puede * realizar esas dos tareas en paralelo mediante procesos de desove, permitiéndoles ejecutar las dos tareas de forma asíncrona y obtener sus resultados al final, pero no es como "mientras el código es sincrónico, tiende a tener un significado diferente al de procedimiento". idiomas ". Ver también la respuesta de Chris. – hcs42

-2

En un lenguaje puramente funcional, el orden de evaluación no importa - en una aplicación de función fn (arg1, .. argn), los n argumentos se pueden evaluar en paralelo. Eso garantiza un alto nivel de paralelismo (automático).

Erlang utiliza un modelo de proceso donde un proceso puede ejecutarse en la misma máquina virtual o en un procesador diferente; no hay forma de saberlo. Eso solo es posible porque los mensajes se copian entre procesos, no hay un estado compartido (mutable). El paralelismo de múltiples procesadores va mucho más allá que el multi-threading, ya que los hilos dependen de la memoria compartida, esto solo puede haber 8 hilos corriendo en paralelo en una CPU de 8 núcleos, mientras que el procesamiento múltiple puede escalar a miles de procesos paralelos.

10

La clave que permite a Erlang escalar está relacionada con la concurrencia.

Un sistema operativo ofrece la simultaneidad mediante dos mecanismos:

  • sistema operativo procesos
  • hilos del sistema operativo

procesos no compartir el estado - un proceso puede no chocar otra por diseño .

Temas de intercambio de subprocesos: un hilo puede bloquear otro por diseño: ese es su problema.

Con Erlang, la máquina virtual utiliza un proceso de sistema operativo y la máquina virtual proporciona concurrencia al programa Erlang no utilizando subprocesos del sistema operativo, sino proporcionando procesos Erlang, es decir, Erlang implementa su propio timeslicer.

Estos procesos de Erlang se comunican entre sí mediante el envío de mensajes (manejados por la VM de Erlang, no por el sistema operativo). Los procesos de Erlang abordan entre sí utilizando un identificador de proceso (PID), que tiene una dirección de tres partes <<N3.N2.N1>>:

  • proceso no N1 en
  • VM N2 en
  • N3 máquina física

Dos procesos en la misma máquina virtual, en distintas máquinas virtuales en la misma máquina o dos máquinas se comunican de la misma manera; por lo tanto, su escalabilidad es independiente del número de máquinas físicas en las que despliega su aplicación (en la primera aproximación).

Erlang es solo threadsafe en un sentido trivial - no tiene hilos. (El lenguaje que es, la VM SMP/multi-core usa una cadena de sistema operativo por núcleo).

15

Es probable que esté mezclando sincrónico con secuencial.

El cuerpo de una función en erlang se está procesando secuencialmente. Entonces, lo que dijo Spencer sobre este "efecto automágico" no es cierto para erlang. Sin embargo, podrías modelar este comportamiento con erlang.

Por ejemplo, puede generar un proceso que calcula el número de palabras en una línea. Como estamos teniendo varias líneas, engendramos uno de esos procesos para cada línea y recibimos las respuestas para calcular una suma.

De esta manera, generamos procesos que realizan los cálculos "pesados" (utilizando núcleos adicionales si están disponibles) y luego recopilamos los resultados.

-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. 

Y esto es lo que parece, cuando nos encontramos esto en el shell: mensajes

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> 
2

Erlang son puramente asíncrono, si desea una respuesta síncrona a su mensaje usted necesita código de forma explícita para eso. Lo que posiblemente se dijo fue que los mensajes en un cuadro de mensaje de proceso se procesan secuencialmente. Cualquier mensaje enviado a un proceso se ubica en el cuadro de mensaje de ese proceso, y el proceso puede elegir un mensaje de ese cuadro y procesarlo y luego pasar al siguiente, en el orden que considere oportuno. Este es un acto muy secuencial y el bloque de recepción hace exactamente eso.

Parece que ha mezclado síncrono y secuencial como mencionó Chris.

Cuestiones relacionadas