programacion funcional ejemplos caracteristicas concurrency functional-programming erlang scalability

concurrency - ejemplos - ¿Cómo/por qué los lenguajes funcionales(específicamente Erlang) escalan bien?



lisp caracteristicas (8)

He estado observando la creciente visibilidad de los lenguajes y características de programación funcional por un tiempo. Los miré y no vi el motivo de la apelación.

Luego, recientemente asistí a la presentación de Kevin Smith "Conceptos básicos de Erlang" en 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 los 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 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?

Comprendo que los creadores de Erlang evitaron intencionalmente el uso de subprocesos para evitar problemas de simultaneidad, pero pensé que la multitracción era necesaria para lograr la escalabilidad.

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


El sistema de cola de mensajes es genial porque produce efectivamente un efecto de "fuego y espera por resultado", que es la parte sincrónica 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. Considera el siguiente código:

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

Considere por un momento que el métodoWithALotOfDiskProcessing () tarda unos 2 segundos en completarse y que el métodoWALALO deNetworkProcessing () tarda aproximadamente 1 segundo en completarse. 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 probarí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.

¿Cuan genial 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.

EDITAR:

Una vez que comprenda este concepto junto con la publicación de Godeke, es fácil imaginar lo fácil que es aprovechar los múltiples procesadores, granjas de servidores, almacenes de datos redundantes y quién sabe qué más.


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 usa 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 multiprocesadores va mucho más allá que el multihilo, 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.


Es probable que esté mezclando sincrónicamente con secuenciales .

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, podría generar un proceso que calcula el número de palabras en una línea. Como estamos teniendo varias líneas, engendramos un proceso para cada línea y recibimos las respuestas para calcular una suma.

De esta forma, generamos procesos que hacen 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 así es como se ve, cuando ejecutamos esto en el 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>


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

Un sistema operativo proporciona concurrencia por dos mecanismos:

  • procesos del sistema operativo
  • hilos del sistema operativo

Los procesos no comparten estado: un proceso no puede bloquear otro por diseño.

Los subprocesos comparten estado: un hilo puede bloquearse por diseño: ese es su problema.

Con Erlang, la máquina virtual utiliza un proceso de sistema operativo y la VM proporciona concurrencia al programa Erlang no mediante el uso de 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 se direccionan entre sí utilizando un ID de proceso (PID) que tiene una dirección de tres partes <<N3.N2.N1>> :

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

Dos procesos en la misma máquina virtual, en diferentes 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).


Los mensajes de Erlang son puramente asíncronos; si desea una respuesta sincrónica a su mensaje, debe codificarlo explícitamente. 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 has mezclado síncrono y secuencial como mencionó Chris.


Puede que tenga un malentendido sobre 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.



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á que los lenguajes funcionales tradicionales al hornear en un sistema de paso de mensajes que permite que todo funcione en un sistema basado en eventos donde un fragmento de código solo se preocupa por recibir mensajes y enviar mensajes, sin preocuparse por una imagen más grande.

Lo que esto significa es que al programador le preocupa (nominalmente) que el mensaje se maneje en otro procesador o máquina: simplemente enviar el 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 mensaje 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.