Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Elixir
Code

¿Qué Es GenServer, y Por Qué Te Debería Importar?

by
Length:LongLanguages:

Spanish (Español) translation by RRGG (you can also view the original English article)

En este artículo aprenderá los conceptos básicos de concurrencia en Elixir y verá cómo generar procesos, enviar y recibir mensajes, y crear procesos de larga ejecución. También aprenderá sobre GenServer, verá cómo se puede usar en su aplicación y descubrirá algunos de los beneficios que le brinda.

Como probablemente sepa, Elixir es un lenguaje funcional utilizado para construir sistemas concurrentes tolerantes a fallas que manejan muchas solicitudes simultáneas. BEAM (máquina virtual Erlang) utiliza procesos para realizar varias tareas al mismo tiempo, lo que significa, por ejemplo, que atender una solicitud no bloquea a otra. Los procesos son livianos y aislados, lo que significa que no comparten memoria e incluso si un proceso falla, otros pueden continuar ejecutándose.

Los procesos BEAM son muy diferentes de los procesos del sistema operativo. Básicamente, BEAM se ejecuta en un proceso de sistema operativo y utiliza sus propios programadores. Cada planificador ocupa un núcleo de CPU, se ejecuta en un subproceso separado y puede manejar miles de procesos simultáneamente (que se turnan para ejecutarse). Puede leer un poco más sobre BEAM y multihilo en StackOverflow.

Entonces, como ve, los procesos de BEAM (diré simplemente "procesos" a partir de ahora) son muy importantes en Elixir. El lenguaje le proporciona algunas herramientas de bajo nivel para generar manualmente procesos, mantener el estado y manejar las solicitudes. Sin embargo, pocas personas los usan; es más común confiar en el marco de la Plataforma de Telecomunicaciones Abiertas (OTP) para hacerlo.

La OTP hoy en día no tiene nada que ver con los teléfonos; es un marco de propósito general para construir sistemas concurrentes complejos. Define cómo deben estructurarse sus aplicaciones y proporciona una base de datos, así como un conjunto de herramientas muy útiles para crear procesos de servidor, recuperarse de errores, realizar el registro, etc. En este artículo, hablaremos sobre un comportamiento del servidor llamado GenServer proporcionado por OTP.

Puede pensar en GenServer como una abstracción o una ayuda que simplifica el trabajo con los procesos del servidor. En primer lugar, verá cómo generar procesos utilizando algunas funciones de bajo nivel. Luego cambiaremos a GenServer y veremos cómo nos simplifica las cosas al eliminar la necesidad de escribir código tedioso (y bastante genérico) todo el tiempo. ¡Empecemos!

Todo Comienza Engendrando

Si me preguntaras cómo crear un proceso en Elixir, respondería: ¡engendra! spawn it spawn / 1 es una función definida dentro del módulo Kernel que devuelve un nuevo proceso. Esta función acepta una lambda que se ejecutará en el proceso creado. Tan pronto como finaliza la ejecución, el proceso también finaliza:

Entonces, aquí spawn devolvió una nueva identificación de proceso. Si agrega un retraso a la lambda, la cadena "hola" se imprimirá después de un tiempo:

Ahora podemos generar tantos procesos como queramos, y se ejecutarán al mismo tiempo:

Aquí estamos generando diez procesos e imprimiendo una cadena de prueba con un número aleatorio. : Rand es un módulo proporcionado por Erlang, por lo que su nombre es un átomo. Lo bueno es que todos los mensajes se imprimirán al mismo tiempo, después de cinco segundos. Sucede porque los diez procesos se están ejecutando al mismo tiempo.

Compárelo con el siguiente ejemplo que realiza la misma tarea pero sin usar spawn / 1:

Mientras se ejecuta este código, puede ir a la cocina y hacer otra taza de café, ya que tomará casi un minuto completarla. Cada mensaje se muestra secuencialmente, lo cual, por supuesto, no es óptimo.

Puede preguntar: "¿Cuánta memoria consume un proceso?" Bueno, depende, pero inicialmente ocupa un par de kilobytes, que es un número muy pequeño (incluso mi laptop anterior tiene 8GB de memoria, por no mencionar los servidores modernos y geniales).

Hasta aquí todo bien. Sin embargo, antes de comenzar a trabajar con GenServer, analicemos otra cosa importante: pasar y recibir mensajes.

Trabajando Con Mensajes

No sorprende que los procesos (que están aislados, como usted recuerde) necesiten comunicarse de alguna manera, especialmente cuando se trata de construir sistemas más o menos complejos. Para lograr esto, podemos usar mensajes.

Se puede enviar un mensaje usando una función con un nombre bastante obvio: send / 2. Acepta un destino (puerto, id de proceso o nombre de proceso) y el mensaje real. Después de enviar el mensaje, aparece en el buzón de un proceso y se puede procesar. Como puede ver, la idea general es muy similar a nuestra actividad diaria de intercambiar correos electrónicos.

Un buzón es básicamente una cola "primero en entrar, primero en salir" (FIFO). Después de que se procesa el mensaje, se elimina de la cola. Para empezar a recibir mensajes, necesita ... ¿adivinen qué? - macro de recepción. Esta macro contiene una o más cláusulas, y un mensaje se compara con ellas. Si se encuentra una coincidencia, el mensaje se procesa. De lo contrario, el mensaje se vuelve a colocar en el buzón. Además de eso, puede establecer una cláusula posterior opcional que se ejecuta si no se recibió un mensaje en el tiempo dado. Puede leer más sobre send / 2 y recibir en los documentos oficiales.

De acuerdo, basta con la teoría, intentemos trabajar con los mensajes. En primer lugar, envíe algo al proceso actual:

La macro self / 0 devuelve un pid del proceso de llamada, que es exactamente lo que necesitamos. No omita los corchetes después de la función, ya que recibirá una advertencia sobre la coincidencia de ambigüedad.

Ahora recibe el mensaje mientras configura la cláusula posterior:

Tenga en cuenta que la cláusula devuelve el resultado de evaluar la última línea, por lo que obtenemos el "¡Hola!" cuerda.

Recuerda que puedes introducir tantas claúsulas como sea posible:

Aquí tenemos cuatro cláusulas: una para manejar un mensaje de éxito, otra para manejar errores, y luego una cláusula de "repliegue" y un tiempo de espera.

Si el mensaje no coincide con ninguna de las cláusulas, se guarda en el buzón, lo que no siempre es deseable. ¿Por qué? Porque cada vez que llega un nuevo mensaje, los antiguos se procesan en el primer encabezado (porque el buzón es una cola FIFO), ralentizando el programa. Por lo tanto, una cláusula de "retroceso" puede ser útil.

Ahora que sabe cómo generar procesos, enviar y recibir mensajes, echemos un vistazo a un ejemplo un poco más complejo que implica crear un servidor simple que responda a varios mensajes.

Trabajando Con Proceso del Servidor

En el ejemplo anterior, enviamos solo un mensaje, lo recibimos y realizamos algún trabajo. Está bien, pero no muy funcional. Por lo general, lo que sucede es que tenemos un servidor que puede responder a varios mensajes. Por "servidor" me refiero a un proceso de larga ejecución construido con una función recurrente. Por ejemplo, creemos un servidor para realizar algunas ecuaciones matemáticas. Recibirá un mensaje que contiene la operación solicitada y algunos argumentos.

Empieza por crear el servidor y la función de looping.

Entonces generamos un proceso que sigue escuchando los mensajes entrantes. Después de recibir el mensaje, se vuelve a llamar a la función listen / 0, creando así un bucle infinito. Dentro de la función listen / 0, agregamos soporte para el mensaje: sqrt, que calculará la raíz cuadrada de un número. El arg contendrá el número real para realizar la operación en contra. Además, estamos definiendo una cláusula de repliegue.

Ahora puedes comenzar el servidor y asignar su proceso id a una variable:

¡Brillante! Ahora agreguemos una función de implementación para realizar el cálculo:

Usa esta función ahora:

Por ahora, simplemente imprime el argumento pasado, así que ajusta tu código de esta manera para hacer una operación matemática:

Ahora, se envía otro mensaje al servidor que contiene el resultado del cálculo.

Lo que es interesante es que la función sqrt / 2 simplemente envía un mensaje al servidor solicitando realizar una operación sin esperar el resultado. Entonces, básicamente, realiza una llamada asincrónica.

Obviamente, queremos obtener el resultado en algún punto en el tiempo. así que codifica otra función pública:

Ahora utilízalo:

¡Funciona! Por supuesto, incluso puede crear un conjunto de servidores y distribuir tareas entre ellos, logrando la concurrencia. Es conveniente cuando las solicitudes no se relacionan entre sí.

Conoce GenServer

Muy bien, hemos cubierto un puñado de funciones que nos permiten crear procesos de servidor de larga ejecución y enviar y recibir mensajes. Esto es genial, pero tenemos que escribir demasiado código repetitivo que inicia un bucle de servidor (inicio / 0), responde a mensajes (escucha / 0 función privada) y devuelve un resultado (ganancia_grab / 0). En situaciones más complejas, también podríamos necesitas poner de principal a un estado compartido o solventar los errores.

Como dije al comienzo del artículo, no hay necesidad de reinventar una bicicleta. En su lugar, podemos utilizar el comportamiento de GenServer que ya proporciona todo el código estándar para nosotros y tiene un gran soporte para los procesos del servidor (como vimos en la sección anterior).

Comportamiento en Elixir es un código que implementa un patrón común. Para usar GenServer, debe definir un módulo de devolución de llamada especial que satisfaga el contrato según lo dictado por el comportamiento. Específicamente, debería implementar algunas funciones de devolución de llamada, y la implementación real depende de usted. Después de escribir las devoluciones de llamada, el módulo de comportamiento puede utilizarlas.

Según lo establecido por los documentos, GenServer requiere la implementación de seis devoluciones de llamada, aunque también tienen una implementación predeterminada. Significa que puede redefinir solo aquellos que requieren alguna lógica personalizada.

Lo primero es lo primero: tenemos que iniciar el servidor antes de hacer cualquier otra cosa, así que pase a la siguiente sección.

Empezando el Servidor

Para demostrar el uso de GenServer, escribamos un CalcServer que permita a los usuarios aplicar varias operaciones a un argumento. El resultado de la operación se almacenará en un estado de servidor y, a continuación, se le podrá aplicar otra operación. O un usuario puede obtener un resultado final de los cálculos.

Primero que todo, emplea el uso macro para enchufar el GenServer:

Ahora necesitaremos redefinir algunas llamadas de vuelta.

El primero es init / 1, que se invoca cuando se inicia un servidor. El argumento pasado se usa para establecer el estado de un servidor inicial. En el caso más simple, esta devolución de llamada debería devolver la tupla {: ok, initial_state}, aunque hay otros posibles valores de retorno como {: stop, reason}, que hacen que el servidor se detenga inmediatamente.

Creo que podemos permitir a los usuarios definir el estado inicial de nuestro servidor. Sin embargo, debemos verificar que el argumento pasado sea un número. Entonces usa una cláusula de guardia para eso:

Ahora, simplemente inicie el servidor utilizando la función start / 3 y proporcione su CalcServer como un módulo de devolución de llamada (el primer argumento). El segundo argumento será el estado inicial:

Si intenta pasar un número no como segundo argumento, el servidor no se iniciará, que es exactamente lo que necesitamos.

¡Estupendo! Ahora que nuestro servidor se está ejecutando, podemos comenzar a codificar las operaciones matemáticas.

Manejo de Solicitudes Asincrónicas

Las solicitudes asíncronas se llaman conversiones en términos de GenServer. Para realizar dicha solicitud, use la función de conversión / 2, que acepta un servidor y la solicitud real. Es similar a la función sqrt / 2 que codificamos cuando hablamos de los procesos del servidor. También utiliza el enfoque "disparar y olvidarse", lo que significa que no estamos esperando que finalice la solicitud.

Para manejar los mensajes asíncronos, se utiliza una devolución de llamada handle_cast / 2. Acepta una solicitud y un estado y debe responder con una tupla {: noreply, new_state} en el caso más simple (o {: stop, reason, new_state} para detener el bucle del servidor). Por ejemplo, manejemos un molde asincrónico: sqrt:

Así es como mantenemos el estado de nuestro servidor. Inicialmente, el número (pasado cuando se inició el servidor) era 5.1. Ahora actualizamos el estado y lo configuramos en: math.sqrt (5.1).

Codifique la función de interfaz que utiliza cast / 2:

Para mí, esto se asemeja a un mago malvado que lanza un hechizo pero no le importa el impacto que causa.

Tenga en cuenta que necesitamos una identificación de proceso para realizar el reparto. Recuerde que cuando un servidor se inicia con éxito, se devuelve una tupla {: ok, pid}. Por lo tanto, utilicemos la coincidencia de patrones para extraer la identificación del proceso:

¡Bonito! El mismo enfoque se puede usar para implementar, por ejemplo, la multiplicación. El código será un poco más complejo ya que tendremos que pasar el segundo argumento, un multiplicador:

La función de conversión solo admite dos argumentos, por lo que necesito construir una tupla y pasar un argumento adicional allí.

Ahora el llamado de vuelta:

También podemos escribir una única devolución de llamada handle_cast que admita la operación, así como detener el servidor si la operación es desconocida:

Ahora usa la nueva función de interfaz:

Genial, pero actualmente no hay forma de obtener un resultado de los cálculos. Por lo tanto, es hora de definir otra devolución de llamada.

Manejando Peticiones Sincronizadas

Si las solicitudes asíncronas son conversiones, las llamadas síncronas reciben el nombre de llamadas. Para ejecutar dichas solicitudes, utilice la función call / 3, que acepta un servidor, una solicitud y un tiempo de espera opcional que equivale a cinco segundos por defecto.

Las solicitudes sincrónicas se utilizan cuando queremos esperar hasta que llegue realmente la respuesta del servidor. El caso típico de uso es obtener cierta información como resultado de los cálculos, como en el ejemplo de hoy (recuerde la función grab_result / 0 de una de las secciones anteriores).

Para procesar solicitudes sincrónicas, se utiliza una devolución de llamada handle_call / 3. Acepta una solicitud, una tupla que contiene el pid del servidor y un término que identifica la llamada, así como también el estado actual. En el caso más simple, debería responder con una tupla {: responder, responder, nuevo_estado}.

Codifica este llamado ahora:

Como ve, nada complejo. La respuesta y el nuevo estado equivalen al estado actual ya que no quiero cambiar nada después de que se devolvió el resultado.

Ahora la función de resultado de la interfaz / 1:

¡Eso es todo! El uso final de CalcServer se demuestra a continuación:

Aliasing

Resulta tedioso proporcionar siempre una identificación del proceso al llamar a las funciones de la interfaz. Afortunadamente, es posible darle a su proceso un nombre o un alias. Esto se hace al inicio del servidor por nombre de configuración:

Tenga en cuenta que no estoy almacenando pid ahora, aunque es posible que desee hacer una coincidencia de patrón para asegurarse de que el servidor realmente se inició.

Ahora las funciones de la interfaz se vuelven un poco más simples:

Solo no te olvides de que no puedes comenzar dos servidores con el mismo alias.

Alternativamente, puede introducir otra función de interfaz start / 1 dentro de su módulo y aprovechar la macro __MODULE __ / 0, que devuelve el nombre del módulo actual como un átomo:

Terminación

Otra devolución de llamada que se puede redefinir en su módulo se llama terminar / 2. Acepta un motivo y el estado actual, y se invoca cuando un servidor está a punto de salir. Esto puede suceder cuando, por ejemplo, pasa un argumento incorrecto a la función de interfaz multiplicar / 1:

La devolución de llamada puede verse más o menos así:

Conclusión

En este artículo, hemos cubierto los conceptos básicos de concurrencia en Elixir y hemos analizado funciones y macros como spawn, receive y send. Has aprendido qué procesos son, cómo crearlos y cómo enviar y recibir mensajes. Además, hemos visto cómo crear un proceso simple de servidor de larga ejecución que responda a mensajes síncronos y asíncronos.

Encima de eso, hemos discutido el comportamiento del GenServer y hemos visto cómo simplifica el código al introducir varios llamados. Hemos trabajado con las devoluciones de llamada init, terminate, handle_call y handle_cast y creamos un servidor de cálculo simple. Si algo no te pareció claro, ¡no dudes en publicar tus preguntas!

Hay más para el GenServer, y por supuesto es imposible cubrir todo en un solo artículo. En mi siguiente publicación. explicaré qué son los supervisores y cómo los puedes usar para monitorear tus procesos y recuperarte de tus errores. Hasta ese entonces, ¡feliz codificación!

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.