Comprendiendo Procesos en la BEAM con Elixir
Este post es para todos los developers que quieren experimentar con Elixir, o bien que están en los primeros pasos con esta tecnología. Este tema requiere que tengas un conocimiento básico de Elixir y Erlang, al menos para comprender por qué es importante dedicarle tiempo de calidad a este tema, porque es sin duda alguna uno de los conceptos centrales de la máquina virtual de Erlang BEAM.
Aunque Elixir es considerado un lenguaje de propósito general, no es necesario que tengas que meterte con este tipo de temas al inicio. Pero si quieres conocer porqué la BEAM y sus lenguajes son tan populares en el mundo de concurrencia y paralelismo, este post es para ayudarte a comprender cómo diseñar software en el ecosistema de Erlang.
¿Por qué entender los procesos?
Seguramente al buscar información sobre Elixir encontrarás grandes características como: sistemas altamente escalables, tolerancia a fallos, sistemas concurrentes y distribuidos, etc. Pues bien, todas estas ventajas vienen directamente de la Máquina Virtual de Erlang (mejor conocida como BEAM Björn’s Erlang Abstract Machine).
Elixir, de nuevo, es un lenguaje de propósito general. Esto quiere decir que puedes aprender a usarlo mediante su paradigma funcional, resolver algunos problemas del día a día, y no tener que meterte con el tema de procesos si así lo deseas. Pero precisamente entender los procesos es una gran forma de entender qué es lo que hace a Elixir un lenguaje tan poderoso y cómo comenzar a aprovechar todas las ventajas de su ecosistema, mismas que lo hacen diferente de otros lenguajes y plataformas.
¿Qué es un proceso en el ecosistema de Erlang?
Un proceso es una entidad aislada donde ocurre la ejecución de código.
Los procesos están en todos lados en cualquier sistema creado en la BEAM, podrías usar cualquier lenguaje como Erlang, Elixir, Gleam o b, y podrás encontrar el mismo concepto. Por ejemplo la shell interactiva iex, o los patrones de OTP son ejemplos de procesos.
Los procesos son parte central para crear programas concurrentes y construir diseños distribuidos y tolerantes a fallos. Aunque, como mencioné anteriormente, podrías solo escribir módulos y funciones, y no tener que preocuparte de cómo funcionan hasta cierto punto.
Quienes están empezando a aprender Elixir seguramente hemos escuchado hablar de OTP “The Open Telecom Platform”, una serie de abstracciones que nos permite explotar las capacidaddes de la BEAM. Entender el tema de procesos es también entender qué pasa “behind the scenes” de estas abstracciones, esto te ayudará a comprender cómo aprovechar estas ventajas al momento de diseñar algún componente de software.
Anatomía de un Proceso
Un proceso es una entidad aislada que permite la ejecución de código, es la base para diseñar software y aprovechar las ventajas que brinda la BEAM.
Un proceso, a nivel de memoria, esta compuesto de cuatro bloques donde se guarda toda la información que permite su ejecución:
- Stack: almacena las variables locales.
- Heap: almacena estructuras de datos largas.
- Mailbox: guarda los mensajes recividos.
- Process Control Block: Permite hacer el tracking del estado interno del proceso.
Ciclo de Vida
Podemos definir de momento el ciclo de vida de un proceso en las siguientes fases:
- Creación
- Ejecución de código
- Finalización (
termination
)
Creación de un Proceso
La función spawn
nos permite crear un proceso, solo necesitaremos pasarle una función que guardará el código que se va a ejecutar dentro del proceso.
Al crear un proceso, obtendremos un process identifier
con el que podremos identificar nuestro proceso. Esto será posible con functiones provistar por el módulo Process
.
Veamos un ejemplo en la iex
:
- Se crea una función para imprimir un
string
. Esta será el código a ejecutar dentro de un proceso.
- Creación de un proceso: Invoca la función
spawn
con la funciónexecute_fun
como parámetro único. Esto creará un nuevo proceso y regresará unPID
.
-
Ejecución de código: inmediatamente al crear el proceso, se ejecutará la función que recibió como parámetro.
-
Terminación: Después de la ejecución de código, el proceso terminará su ejecución. Para verificar esto puedes usar la función
Process.alive?/1
que te regresará un valor booleano para indicar si el proceso sigue vivo o no.
Interacción entre Procesos: Recepción de Mensajes
Es común entonces diseñar código para ejecutarse dentro de un proceso de forma individual, así es que será común tener que ejecutar diferentes funcionalidades en procesos por serapado que tendrán que interactuar entre sí en algún punto en el tiempo, para ello existen los mensajes.
Un proceso es capaz de ejecutar código de forma aislada así como de recibir mensajes provenientes de otros procesos. Los mensajes son la única forma de comunicarse entre ellos, estos se guardan en el mailbox, uno de los cuatro bloques de memoria que componen a un proceso.
Es importante aclarar que los mensajes recibidos o enviados son independientes del código que se ejecuta.
Para procesar los mensajes recibidos es necesario usar la sentencia receive
. Mientras que podrás enviar mensajes con la sentencia send
. Para ello nos sirve el Process Identifier
, pues es la forma de identificar cada proceso y poder comunicarse con él. Veamos el siguiente ejemplo:
- Se creará una función dentro de un módulo
MyProcess.awaiting_for_receive_messages/0
que implementará la sentenciareceive
. Esto nos ayudará a indicarle al proceso que ejecuta ciertas acciones al recibir cierto tipo de mensajes provenientes de otro proceso.
Este código puedes copiar y pegar directamente sobre la iEx
.
- Creación de un proceso: Tomaremos la función anterior para crear un nuevo proceso, esto hará
que la función
receive
se ejecutesolo al recibir un mensaje
, entonces terminará la ejecución y terminará. De lo contrario el proceso no morirá, mientras la sentenciareceive
espera a ejecutarse.
- Envío de mensajes: Cabe señalar que la
iEx
es un proceso en sí, por lo que podemos enviar mensajes desde aquí al proceso que acabamos de crear. Usaremos la sentenciasend
, enviaremos elPID
del proceso al que le vamos a enviar el mensaje, y el segundo parámetro será el contenido del mismo, que para este caso será un string pero podría ser otro dato.
-
Ejecución de código: Al recibir el mensaje, inmediatamente se ejecutará nuestra función con el
receive
. -
Terminación: Al recibir el mensaje y procesarlo, nuestro proceso habrá concluido. ¿Pero y qué pasa si queremos mantener ese proceso aún con vida?
Mantener un Proceso Activo
En el ejemplo anterior, nuestro proceso terminará su vida al procesar el mensaje. Es posible que un proceso siga activo incluso después de procesar un mensaje, para ello podemos hacer uso de la recursividad para volver a invocar la función con el receive
. Esto nos ayudará también a poder conservar un estado, como lo veremos más adelante.
Mantener un Estado
Retomando el ejemplo anterior, es posible conservar un estado que puede ir transformándose. A continuación te muestro un ejemplo de cómo quedaría nuestro módulo de ejemplo:
En este caso ahora nuestra función awaiting_for_receive_messages
reciben una lista vacía donde guardaremos los mensajes recibidos.
- Creación del proceso:
pid = spawn(MyProcess, :awaiting_for_receive_messages, [])
- Recepción de un mensaje:
send(pid, "Hi")
después de invocar esto, podemos verificar que el proceso siga activo. - Recepción de más mensajes: podremos repetir el paso anterior muchas veces.
¿Dónde están los procesos?
Muy bien, si me acompañaste hasta este punto hemos recorrido algunas nociones básicas sobre qué es un proceso y cómo funciona. Si te preguntas dónde puedes ver hasta donde llegar con estas implementaciones te doy un gran spoiler: processes everywhere
.
Veamos un ejemplo de Phoenix Live Views, este es un módulo que tiene efecto sobre un template de html (no te preocupes por el nombre de las funciones):
No me gustaría entrar en mucho detalle con los Live Views, pero si contarte que este módulo sirve para dos cosas, por un lado las funciones render/1
y mount/3
sirven para hacer el setup de la Live View
, las funciones handle_info
y handle_event
permiten mantener activa la conexión del socket y un estado interno ¿te suena esto familiar? ¡Es un proceso! Un Live View
es una abstracción de OTP para crear un proceso.
Processes Everywhere
Entender cómo funcionan los procesos te dará mejores conceptos para entender cómo funciona el Ecosistema de Erlang y para diseñar mejores programas. Muchas bibliotecas y proyectos escritos en Elixir usan estos conceptos, entonces la próxima vez que uses algún proyecto, piensa que probablemente estarás usando abstracciones de procesos por debajo.
Si quieres conocer más de este tema te sugiero probar el código de ejemplo en este post, y buscar los apartados de procesos dentro de los libros de Elixir o Erlang. Probablemente también la documentación de Erlang te dará aún más conceptos sobre este gran tema.
Muchas gracias por llegar hasta aquí, si quieres conocer más de este gran mundo puedes ver las opciones de training y las próximas charlas de la Code Beam América.