Bienvenidos al segundo capítulo de la serie “
Elixir, 7 pasos para iniciar tu viaje”
.
En el
primer capítulo
hablamos sobre la máquina virtual de Erlang, la BEAM, y las características que Elixir aprovecha de ella para desarrollar sistemas que son:
-
Concurrentes
-
Tolerantes a fallos
-
Escalables y
-
Distribuidos
En esta nota explicaremos qué significa la concurrencia para Elixir y Erlang y por qué es importante para desarrollar sistemas tolerantes a fallos. Al final encontrarás un pequeño ejemplo de código hecho con Elixir para que puedas observar las ventajas de la concurrencia en acción.
Concurrencia
La concurrencia es la habilidad para llevar a cabo dos o más tareas
aparentemente
al mismo tiempo .
Para entender por qué la palabra
aparentemente
está resaltada, veamos el siguiente caso:
Una persona tiene que completar dos actividades, la
tarea A
y la
tarea B
-
Inicia la tarea A, avanza un poco y la pausa.
-
Inicia la tarea B, avanza un poco, la pausa y continúa con la tarea A.
-
Avanza un poco con la tarea A, la pausa y continúa con la tarea B.
Y así va avanzando con cada una, hasta terminar ambas actividades.
No es que la
tarea A
y la
tarea B
se lleven a cabo exactamente al mismo tiempo, más bien la persona dedica un tiempo a cada una y va intercambiándose entre ellas. Estos tiempos pueden ser tan cortos que el cambio es imperceptible para nosotros, por eso se produce la ilusión de que las actividades están sucediendo simultáneamente.
Paralelismo
Hasta ahora no había mencionado nada sobre paralelismo porque no es un concepto fundamental en la BEAM o para Elixir. Pero recuerdo que cuando estaba aprendiendo a programar se me dificultó comprender la diferencia entre paralelismo y concurrencia, así que aprovecharé esta nota para compartirte una breve explicación.
Sigamos con el ejemplo anterior. Si ahora traemos a otra persona para completar las tareas y ambas trabajan al mismo tiempo,
hablamos de paralelismo
.
De manera que podríamos tener a dos o más personas trabajando paralelamente, cada una llevando a cabo sus actividades concurrentemente.
Es decir, la concurrencia puede ser o no paralela.
En Elixir la concurrencia se logra gracias a los procesos de Erlang, que son creados y administrados por la BEAM.
Procesos
En Elixir todo el código se ejecuta dentro de procesos. Y una aplicación puede tener cientos o miles de ellos ejecutándose de manera concurrente.
¿Cómo funciona?
Cuando la BEAM se ejecuta en una máquina, se encarga de crear por default un hilo
en cada procesador disponible. En ese hilo existe una cola dedicada a tareas específicas, y cada cola tiene a su vez un administrador (
scheduler
) que es responsable de asignar un tiempo y una prioridad a las tareas.
Entonces, en una máquina
multicore
con dos procesadores puedes tener dos hilos y dos
schedulers
, lo que te permite paralelizar las tareas al máximo. También puedes ajustar la configuración de la BEAM para indicarle qué procesadores utilizar.
En cuanto a las tareas, cada una se ejecuta en un proceso aislado.
Parece algo simple, pero justamente esta idea es la magia detrás de la escalabilidad, distribución y tolerancia a fallos de un sistema hecho con Elixir.
Veamos este último concepto para entender por qué.
Tolerancia a fallos
La tolerancia a fallos de un sistema se refiere a la capacidad que tiene para manejar los errores y
no morir en el intento.
El objetivo es que ninguna falla, sin importar lo crítica que sea, inhabilite o bloquee el sistema. Esto se logra nuevamente gracias a los procesos de Erlang.
Los procesos son elementos aislados, que no comparten memoria y se comunican mediante paso de mensajes.
Lo anterior significa que si algo falla en el proceso A, el proceso B no se ve afectado, es más, es posible que ni siquiera se entere. El sistema seguirá funcionando con normalidad mientras la falla se arregla
tras bambalinas
. Y si a esto sumamos que la BEAM también nos proporciona por defecto mecanismos para detección y recuperación de errores podemos garantizar que el sistema funcione de manera ininterrumpida.
Si quieres explorar más acerca del funcionamiento de los procesos, puedes consultar esta nota:
Understanding Processes for Elixir Developers
.
¿Cómo se ve esto en Elixir?
¡Por fin llegamos al código!
Revisemos un ejemplo de cómo crear procesos que se ejecutan de manera concurrente en Elixir. Lo vamos a contrastar con el mismo ejercicio ejecutándose de manera secuencial.
¿Listo? No te preocupes si no entiendes algo de la sintaxis, en general el lenguaje es muy intuitivo, pero el objetivo por ahora es que seas testigo de la magia de la concurrencia en acción.
El primer paso consiste en crear los procesos.
Spawn
Hay diferentes formas de crear procesos en Elixir. A medida que vayas avanzando encontrarás maneras más sofisticadas de hacerlo, aquí utilizaremos la básica:
la función spawn
. ¡Manos a la obra!
Tenemos 10 registros que corresponden a la información de usuarios que vamos a insertar en una base de datos, pero antes queremos validar que el nombre no contenga caracteres raros y que el email tenga un @.
Supongamos que la validación de cada usuario tarda en total 2 segundos.
-
Abre un editor de texto y copia el siguiente código. Guárdalo en un archivo llamado
procesos.ex
defmodule Procesos do
# Vamos a utilizar expresiones regulares para el formato del nombre y
# el correo electrónico
@email_valido ~r/^([a-zA-Z0-9_\-\.\+]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$/
@nombre_valido ~r/\b([A-ZÀ-ÿ][-,a-z. ']+[ ]*)+/
# Se tiene una lista de usuarios con un nombre y correo electrónico.
# La función validar_usuarios_X manda a llamar a otra función:
# validar_usuario, que revisa el formato del correo e imprime un
# mensaje de ok o error para cada registro
# Esta función trabaja SECUENCIALMENTE
def validar_usuarios_secuencialmente() do
IO.puts("Validando usuarios secuencialmente...")
usuarios = crear_usuarios()
Enum.each(usuarios, fn elem ->
validar_usuario(elem) end)
end
# Esta función trabaja CONCURRENTEMENTE, utilizando spawn
def validar_usuarios_concurrentemente() do
IO.puts("Validando usuarios concurrentemente...")
usuarios = crear_usuarios()
Enum.each(usuarios, fn elem ->
spawn(fn -> validar_usuario(elem) end)
end)
end
def validar_usuario(usuario) do
usuario
|> validar_email()
|> validar_nombre()
|> imprimir_estatus()
# Esto hace una pausa de 2 segundos para simular que el proceso inserta # los registros en base de datos
Process.sleep(2000)
end
# Esta función recibe un usuario, valida el formato del correo y le
# agrega la llave email_valido con el resultado.
def validar_email(usuario) do
if Regex.match?(@email_valido, usuario.email) do
Map.put(usuario, :email_valido, true)
else
Map.put(usuario, :email_valido, false)
end
end
# Esta función recibe un usuario, valida su nombre y le agrega la
# llave nombre_valido con el resultado.
def validar_nombre(usuario) do
if Regex.match?(@nombre_valido, usuario.nombre) do
Map.put(usuario, :nombre_valido, true)
else
Map.put(usuario, :nombre_valido, false)
end
end
# Esta función recibe un usuario que ya pasó por la validación
# de email y nombre y dependiendo de su resultado, imprime el
# mensaje correspondiente al estatus.
def imprimir_estatus(%{
id: id,
nombre: nombre,
email: email,
email_valido: email_valido,
nombre_valido: nombre_valido
}) do
cond do
email_valido && nombre_valido ->
IO.puts("Usuario #{id} | #{nombre} | #{email} ... es válido")
email_valido && !nombre_valido ->
IO.puts("Usuario #{id} | #{nombre} | #{email} ... tiene un nombre inválido")
!email_valido && nombre_valido ->
IO.puts("Usuario #{id} | #{nombre} | #{email} ... tiene un email inválido")
!email_valido && !nombre_valido ->
IO.puts("Usuario #{id} | #{nombre} | #{email} ... es inválido")
end
end
defp crear_usuarios do
[
%{id: 1, nombre: "Melanie C.", email: "melaniec@test.com"},
%{id: 2, nombre: "Victoria Beckham", email: "victoriab@testcom"},
%{id: 3, nombre: "Geri Halliwell", email: "gerih@test.com"},
%{id: 4, nombre: "123456788", email: "melb@test.com"},
%{id: 5, nombre: "Emma Bunton", email: "emmab@test.com"},
%{id: 6, nombre: "Nick Carter", email: "nickc@test.com"},
%{id: 7, nombre: "Howie Dorough", email: "howie.dorough"},
%{id: 8, nombre: "", email: "ajmclean@test.com"},
%{id: 9, nombre: "341AN L1ttr377", email: "Brian-Littrell"},
%{id: 10, nombre: "Kevin Richardson", email: "kevinr@test.com"}
]
end
end
2. Abre una terminal, escribe
iex
y compila el archivo que acabamos de crear.
$ iex
Erlang/OTP 25 [erts-13.1.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]
Interactive Elixir (1.14.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> c("procesos.ex")
[Procesos]
3. Una vez que hayas hecho esto, manda a llamar la función que valida los registros secuencialmente. Tomará un poco de tiempo, ya que cada registro tarda 2 segundos.
iex(2)> Procesos.validar_usuarios_secuencialmente
4. Ahora manda a llamar la función que valida los registros concurrentemente y observa la diferencia en tiempos.
iex(3)> Procesos.validar_usuarios_concurrentemente
Es bastante notoria, ¿no crees? Esto se debe a que en el paso 3, con la evaluación secuencial, cada proceso tiene que esperar a que el anterior termine. En cambio, la ejecución concurrente crea procesos que funcionan aisladamente; por lo tanto, ninguno depende del anterior ni está bloqueado por ninguna otra tarea.
¡Imagina la diferencia cuando se trata de miles o millones de tareas en un sistema!
La concurrencia es la base para las otras características que mencionamos al inicio: distribución, escalabilidad y tolerancia a fallos. Gracias a la BEAM, se vuelve relativamente fácil implementarla en Elixir y aprovechar las ventajas que nos brinda.
Ahora, ya conoces más sobre procesos y concurrencia, especialmente sobre la importancia de este aspecto para crear sistemas altamente confiables y tolerantes a fallas. Recuerda practicar y volver a esta nota cuando lo necesites.
Siguiente capítulo…
En la siguiente nota hablaremos de las bibliotecas,
frameworks
y todos los recursos que existen alrededor de Elixir. Te sorprenderá lo fácil y rápido que es crear un proyecto desde cero y verlo funcionando.
Documentación y Recursos
Consejero técnico:
Raúl Chouza
Corrección de estilo:
Si tienes dudas acerca de esta nota o te gustaría profundizar en el tema, puedes escribirme por Twitter a
@loreniuxmr
.
¡Nos vemos en el siguiente capítulo!
The post
Entendiendo procesos y concurrencia
appeared first on
Erlang Solutions
.