tutorial programacion imagenes erlang

programacion - erlang tutorial



OTP: mensajería sincrónica frente a asíncrona (4)

Descargo de responsabilidad: erlang newbie.

Una de las cosas que me atrajo a Erlang en primer lugar es el modelo Actor; la idea de que diferentes procesos se ejecutan simultáneamente e interactúan a través de mensajes asincrónicos.

Estoy comenzando a meter mis dientes en OTP y en particular mirando gen_server. Todos los ejemplos que he visto, y con la handle_call() que son ejemplos de tipo de tutorial, usan handle_call() lugar de handle_cast() para implementar el comportamiento del módulo.

Encuentro eso un poco confuso. Por lo que puedo decir, handle_call es una operación sincrónica: la persona que llama está bloqueada hasta que el destinatario se complete y regrese. Lo cual parece ir en contra de la filosofía de aprobación de mensaje asíncrono.

Estoy a punto de comenzar una nueva aplicación OTP. Esto parece una decisión arquitectónica fundamental, así que quiero estar seguro de que lo entiendo antes de embarcarme.

Para ser específico, mis preguntas son:

  1. En la práctica, ¿las personas tienden a usar handle_call en lugar de handle_cast?
  2. Si es así, ¿cuál es el impacto de escalabilidad cuando varios clientes pueden llamar al mismo proceso / módulo?

Gracias.


  1. Depende de tu situación

    Si desea obtener un resultado, handle_call es realmente común. Si no está interesado en el resultado de la llamada, use handle_cast . Cuando se usa handle_call , la persona que llama se bloqueará, sí. Esto es la mayor parte del tiempo bien. Echemos un vistazo a un ejemplo.

    Si tiene un servidor web, que devuelve los contenidos de los archivos a los clientes, podrá manejar varios clientes. Cada cliente tiene que esperar a que se lea el contenido de los archivos, por lo que usar handle_call en dicho escenario sería perfectamente handle_call (aparte del estúpido ejemplo).

    Cuando realmente necesita el comportamiento de enviar una solicitud, hacer otro procesamiento y luego obtener la respuesta más tarde, normalmente se usan dos llamadas (por ejemplo, un reparto y una llamada para obtener el resultado) o el envío de mensajes normales. Pero este es un caso bastante raro.

  2. Usar handle_call bloqueará el proceso durante la duración de la llamada. Esto hará que los clientes hagan cola para obtener sus respuestas y, por lo tanto, todo se ejecutará en secuencia.

    Si quiere un código paralelo, debe escribir un código paralelo. La única forma de hacerlo es ejecutar múltiples procesos.

Entonces, para resumir:

  • El uso de handle_call bloqueará a la persona que llama y ocupará el proceso llamado durante la duración de la llamada.
  • Si desea que las actividades paralelas continúen, debe paralelizar. La única forma de hacerlo es iniciando más procesos, y de repente la llamada vs cast ya no es un problema tan grande (de hecho, es más cómodo con la llamada).

IMO, en el mundo concurrente handle_call generalmente es una mala idea. Digamos que tenemos el proceso A (gen_server) recibiendo algún evento (el usuario presiona un botón), y luego enviando un mensaje al proceso B (gen_server) solicitando un procesamiento pesado de este botón presionado. El proceso B puede engendrar el subproceso C, que a su vez envía el mensaje a A cuando está listo (de a B, que envía el mensaje a A). Durante el tiempo de procesamiento, tanto A como B están listos para aceptar nuevas solicitudes. Cuando A recibe un mensaje de transmisión de C (o B), por ejemplo, muestra el resultado al usuario. Por supuesto, es posible que el segundo botón se procese antes que el primero, por lo que A debería acumular los resultados en el orden correcto. El bloqueo de A y B a través de handle_call hará que este sistema tenga un solo hilo (aunque resolverá el problema de pedido)

De hecho, el desove C es similar a handle_call , la diferencia es que C es altamente especializado, procesa solo "un mensaje" y sale después de eso. Se supone que B tiene otra funcionalidad (por ejemplo, límite de número de trabajadores, tiempos de espera de control); de lo contrario, C podría generarse a partir de A.

Editar: C también es asíncrono, por lo que generar C no es similar a handle_call (B no está bloqueado).


La respuesta de Adam es genial, pero tengo un punto para agregar

Usar handle_call bloqueará el proceso durante la duración de la llamada.

Esto es siempre cierto para el cliente que realizó la llamada handle_call . Esto me llevó un tiempo entenderlo, pero esto no significa necesariamente que gen_server también tenga que bloquearse al responder el handle_call.

En mi caso, me encontré con esto cuando creé una base de datos manejando gen_server y deliberadamente escribí una consulta que ejecutaba SELECT pg_sleep(10) , que es PostgreSQL-speak para "dormir por 10 segundos", y era mi forma de probar consultas muy costosas . Mi desafío: ¡No quiero que la base de datos gen_server se quede allí esperando que termine la base de datos!

Mi solución fue usar gen_server: reply / 2 :

Esta función puede ser utilizada por un gen_server para enviar explícitamente una respuesta a un cliente que llamó a call / 2,3 o multi_call / 2,3,4, cuando la respuesta no se puede definir en el valor de retorno de Module: handle_call / 3.

En codigo:

-module(database_server). -behaviour(gen_server). -define(DB_TIMEOUT, 30000). <snip> get_very_expensive_document(DocumentId) -> gen_server:call(?MODULE, {get_very_expensive_document, DocumentId}, ?DB_TIMEOUT). <snip> handle_call({get_very_expensive_document, DocumentId}, From, State) -> %% Spawn a new process to perform the query. Give it From, %% which is the PID of the caller. proc_lib:spawn_link(?MODULE, query_get_very_expensive_document, [From, DocumentId]), %% This gen_server process couldn''t care less about the query %% any more! It''s up to the spawned process now. {noreply, State}; <snip> query_get_very_expensive_document(From, DocumentId) -> %% Reference: http://www.erlang.org/doc/man/proc_lib.html#init_ack-1 proc_lib:init_ack(ok), Result = query(pgsql_pool, "SELECT pg_sleep(10);", []), gen_server:reply(From, {return_query, ok, Result}).


Hay dos formas de hacerlo. Una es cambiar a usar un enfoque de gestión de eventos. El que estoy usando es usar cast como se muestra ...

submit(ResourceId,Query) -> %% %% non blocking query submission %% Ref = make_ref(), From = {self(),Ref}, gen_server:cast(ResourceId,{submit,From,Query}), {ok,Ref}.

Y el código de transmisión / envío es ...

handle_cast({submit,{Pid,Ref},Query},State) -> Result = process_query(Query,State), gen_server:cast(Pid,{query_result,Ref,Result});

La referencia se utiliza para rastrear la consulta de forma asincrónica.