2011-05-19 20 views
15

En this ejemplo, el autor evita una situación de bloqueo haciendo:¿Es malo enviar un mensaje a self() in init?

self() ! {start_worker_supervisor, Sup, MFA}

en función init de su gen_server. Hice algo similar en uno de mis proyectos y me dijeron que este método estaba mal visto, y que era mejor provocar un tiempo de espera inmediato. ¿Cuál es el patrón aceptado?

Respuesta

13

Actualización para Erlang 19+

Considere el uso de la nueva conducta gen_statem. Este comportamiento admite la generación de eventos internos al FSM:

La función de estado puede insertar eventos utilizando la acción() próximo_evento y dicho evento se inserta como el siguiente para presentar a la función de estado. Es decir, como si fuera el evento entrante más antiguo. Un event_type() interno dedicado se puede usar para tales eventos, haciendo que sea imposible confundirlos con eventos externos.

La inserción de un evento reemplaza el truco de invocar sus propias funciones de manejo de estado a las que a menudo tendría que recurrir, por ejemplo, gen_fsm para forzar el procesamiento de un evento insertado antes que otros.

Utilizando el action functionality en ese módulo, puede asegurarse de que su evento se genera en init y siempre manejado antes de que los acontecimientos externos, especialmente mediante la creación de una acción next_event en su función init.

Ejemplo:

... 

callback_mode() -> state_functions. 

init(_Args) -> 
    {ok, my_state, #data{}, [{next_event, internal, do_the_thing}]} 

my_state(internal, do_the_thing, Data) -> 
    the_thing(), 
    {keep_state, Data); 
my_state({call, From}, Call, Data) -> 
    ... 

... 

respuesta Antiguo

Al diseñar un gen_server por lo general, tiene la opción de realizar acciones en tres estados diferentes:

  • al poner en marcha, en init/1
  • Al ejecutar, en cualquier función handle_*
  • Cuando se detenga, en terminate/2

Una buena regla de oro es para ejecutar las cosas en las funciones de manejo al actuar sobre un evento (llamar, yeso, mensaje, etc.). Las cosas que se ejecutan en init no deben esperar a los eventos, para eso sirven las devoluciones de llamada.

Por lo tanto, en este caso particular, se genera un tipo de evento "falso". Diría que parece que el gen_server siempre quiere iniciar el inicio del supervisor. ¿Por qué no solo hacerlo directamente en init/1? ¿Existe realmente un requisito para poder manejar otro mensaje en el medio (el efecto de hacerlo en el handle_info/2 en su lugar)? Esa ventana es increíblemente pequeña (el tiempo entre el inicio del gen_server y el envío del mensaje al self()) por lo que es muy poco probable que suceda.

En cuanto al punto muerto, realmente recomendaría no llamar a su propio supervisor en su función init. Eso es solo una mala práctica. Un buen patrón de diseño para iniciar el proceso de trabajo sería un supervisor de nivel superior, con un gerente y un supervisor de trabajador debajo. El gerente comienza trabajadores mediante una llamada al supervisor del trabajador:

[top_sup] 
    | \ 
    |  \ 
    |  \ 
man [work_sup] 
    /| \ 
    / | \ 
    / | \ 
    w1 ... wN 
+0

No recomendaría mover cosas a init/1 simplemente porque generalmente se repite el código. Claro, podríamos extraerlo a la función interna, pero luego el código en handle_call es menos legible. – user425720

+1

Bueno, si es código repetido, lo pondría en una función y luego lo llamaría desde 'init/1' y su' handle_call/3' o 'handle_cast/2'. Veo que las llamadas a funciones son mucho más legibles que el envío de mensajes a 'self()'. –

0

Esto puede ser una solución muy eficiente y simple, pero creo que no es un buen estilo erlang. Estoy usando el temporizador: apply_after, que es mejor y no da la impresión de interactuar con el módulo externo/gen_ *.

Creo que la mejor manera sería usar máquinas de estado (gen_fsm). La mayoría de nuestros gen_srvers son realmente máquinas de estados, sin embargo, debido al esfuerzo de trabajo inicial para configurar get_fsm, creo que terminamos con gen_srv.

Para concluir, utilizaría el temporizador: apply_after para hacer que el código sea claro y eficiente o gen_fsm para ser puro estilo Erlang (incluso más rápido).

Acabo de leer los fragmentos de código, pero el ejemplo en sí mismo está de alguna manera roto - No entiendo esta construcción del supervisor manipulador gen_srv. Incluso si es el administrador de un grupo de futuros hijos, esta es una razón aún más importante para hacerlo explícitamente, sin contar con la magia del buzón de los procesos. Depurar esto también sería un infierno en algún sistema más grande.

+0

timer: apply_after es solo una solución, pero la verdadera pregunta es si el envío a self() en init muestra una falla fundamental en el diseño. También gen_fsm no resolverá esto y, en general, generalmente no se recomienda para gen_server. –

+0

¿por qué fsm no lo resolvería? El punto entero es ejecutar algo y seguir siendo compatible con la especificación init/1, que se espera que devuelva el estado. Por lo tanto, podríamos devolver el estado inicial y el estado inicial ejecutando init pesado anterior. – user425720

+0

@ user4: porque la función de estado no se llamará de inmediato solo en llamada o transmisión o tiempo de espera. Exactamente como lo hace gen_server. Podría desencadenar la inicialización con un tiempo de espera corto (por ejemplo, 0) devuelto desde init, pero también podría hacerlo con un gen_server. –

6

Situaciones propio supervisor seguro no parece una mala idea, pero hacer algo similar todo el tiempo.

init(...) -> 
    gen_server:cast(self(), startup), 
    {ok, ...}. 

handle_cast(startup, State) -> 
    slow_initialisation_task_reading_from_disk_fetching_data_from_network_etc(), 
    {noreply, State}. 

creo que esto es más claro que el uso de tiempo de espera y handle_info, es casi seguro que ningún mensaje puede salir adelante del mensaje de inicio (que nadie más tiene nuestra pid hasta después de que hemos enviado ese mensaje), y no interfiere si necesito usar tiempos de espera para otra cosa.

+3

Si su 'gen_server' se inició con' gen_server: start_link/4' o 'gen_server: start/4', el registro ocurre antes de que se llame a' Module: init/1'. Cualquiera que busque el 'gen_server' con su nombre registrado podrá encontrarlo y enviarle un mensaje que llegue antes que el suyo. – r3m0t

7

Sólo para complementar lo que ya se ha dicho sobre la división de una inicialización servidores en dos partes, la primera en la función init/1 y el segundo en cualquiera handle_cast/2 o handle_info/2. En realidad, hay una sola razón para hacer esto y eso es si se espera que la inicialización lleve mucho tiempo. Luego, dividirlo permitirá que el gen_server:start_link regrese más rápido, lo que puede ser importante para los servidores iniciados por los supervisores, ya que se "cuelgan" mientras comienzan sus hijos y un niño de arranque lento puede retrasar el inicio de todo el supervisor.

En este caso, no creo que sea malo dividir la inicialización del servidor.

Es importante tener cuidado con los errores. Un error en init/1 hará que el supervisor termine mientras que hay un error en la segunda parte, ya que harán que el supervisor intente reiniciar a ese niño.

yo personalmente creo que es mejor estilo para que el servidor envíe un mensaje a sí mismo, ya sea con una explícita ! o una gen_server:cast, al igual que con un buen mensaje descriptivo, por ejemplo init_phase_2, será más fácil ver lo que está pasando encendido, en lugar de un tiempo de espera más anónimo. Especialmente si los tiempos de espera también se usan en otros lugares.

+0

Un buen punto para permitir que el supervisor comience a monitorear el proceso. Tal vez los comportamientos deberían haber tenido una devolución de llamada 'second_init' para empezar? :-) –

0

Francamente, no veo un punto en la inicialización de división. Si realiza un levantamiento pesado en init cuelga el supervisor, pero usando timeout/handle_info, enviando el mensaje al self() o agregando init_check a cada controlador (otra posibilidad, aunque no muy conveniente) suspenderá los procesos de llamada. Entonces, ¿por qué necesito un supervisor "que trabaja" con gen_server "no funciona"? La implementación limpia probablemente debería incluir la respuesta "no reparada" para cualquier mensaje durante la inicialización (por qué no engendrar la inicialización completa desde init + enviar mensaje a self() cuando se complete, lo que restablecería el estado "no reparada"). procesado correctamente por la persona que llama y esto agrega mucha complejidad. Solo suspender una respuesta no es una buena idea.

+1

Imagínese a un supervisor que comienza 100 niños cada uno haciendo un medio segundo de inicialización en red. Un minuto de latencia del supervisor es malo, pero los clientes probablemente puedan tolerar medio segundo de retraso del gen_server. – cthulahoops

+0

@cthulahoops Puedo imaginar eso (aunque no entiendo muy bien cuál es la naturaleza de tales conexiones, ¿100 oyentes?), Pero medio segundo no es el tiempo de la CPU, creo. Entonces, ¿por qué no comenzar todos los 100 en paralelo? Será el mismo medio segundo. –

+0

El objetivo es iniciarlos todos en paralelo, pero el supervisor no iniciará el segundo hijo hasta que el primero haya regresado de init. Por lo tanto, es útil para que init regrese rápidamente, incluso si los gen_servers bloquearán las solicitudes durante un breve período de tiempo. – cthulahoops

Cuestiones relacionadas