Procesos
El corazón de la concurrencia en Elixir: procesos ligeros y comunicación por mensajes.
¿Qué son los procesos en Elixir?
Los procesos en Elixir no son procesos del sistema operativo. Son extremadamente ligeros (solo ~2KB de memoria inicial) y son manejados por la máquina virtual BEAM:
- Puedes ejecutar millones de procesos simultáneamente
- Cada proceso tiene su propia memoria (aislamiento total)
- Se comunican solo mediante paso de mensajes
- Si un proceso falla, otros no se ven afectados
Elixir sigue el modelo de actores: cada proceso es un actor independiente que recibe mensajes, los procesa, y puede enviar mensajes a otros actores. No hay memoria compartida.
Crear procesos con spawn
# spawn crea un nuevo proceso y retorna su PID
pid = spawn(fn ->
IO.puts("¡Hola desde otro proceso!")
end)
# El PID identifica al proceso
iex> pid
#PID<0.110.0>
# Verificar si está vivo
iex> Process.alive?(pid)
false # Ya terminó su trabajo
# El proceso actual
iex> self()
#PID<0.105.0>
Enviar y recibir mensajes
Los procesos se comunican mediante send y receive:
# Proceso que espera un mensaje
pid = spawn(fn ->
receive do
{:saludo, nombre} ->
IO.puts("¡Hola, #{nombre}!")
mensaje ->
IO.puts("Recibido: #{inspect(mensaje)}")
end
end)
# Enviar mensaje
send(pid, {:saludo, "Ana"})
# Imprime: ¡Hola, Ana!
Responder al remitente
# Incluir self() para recibir respuesta
pid = spawn(fn ->
receive do
{:calcular, a, b, remitente} ->
send(remitente, {:resultado, a + b})
end
end)
# Enviar y esperar respuesta
send(pid, {:calcular, 5, 3, self()})
receive do
{:resultado, valor} -> IO.puts("Resultado: #{valor}")
end
# Imprime: Resultado: 8
Procesos que mantienen estado
Los procesos pueden mantener estado usando recursión:
defmodule Contador do
def iniciar(valor_inicial \\ 0) do
spawn(fn -> loop(valor_inicial) end)
end
defp loop(estado) do
receive do
:incrementar ->
loop(estado + 1)
:decrementar ->
loop(estado - 1)
{:obtener, remitente} ->
send(remitente, {:valor, estado})
loop(estado) # Continuar con el mismo estado
:detener ->
IO.puts("Contador detenido con valor: #{estado}")
# No llamar loop() = proceso termina
end
end
end
# Uso
iex> pid = Contador.iniciar(10)
iex> send(pid, :incrementar)
iex> send(pid, :incrementar)
iex> send(pid, {:obtener, self()})
iex> receive do {:valor, v} -> v end
12
Timeout en receive
receive do
mensaje -> IO.puts("Recibido: #{mensaje}")
after
5000 -> IO.puts("Timeout después de 5 segundos")
end
Vincular procesos con spawn_link
spawn_link crea un proceso vinculado. Si uno muere, el otro también:
# Con spawn normal, el proceso padre no se entera del crash
spawn(fn -> raise "¡Error!" end)
# El proceso padre sigue vivo
# Con spawn_link, el crash se propaga
spawn_link(fn -> raise "¡Error!" end)
# El proceso padre también muere
Los vínculos son la base de la tolerancia a fallos en Elixir. Permiten que los supervisores detecten cuando un proceso hijo falla y tomen acción (reiniciarlo, por ejemplo).
Monitorear procesos
Los monitores son como vínculos unidireccionales. Recibes una notificación si el proceso monitoreado muere:
pid = spawn(fn ->
Process.sleep(1000)
IO.puts("Proceso terminado")
end)
# Monitorear el proceso
ref = Process.monitor(pid)
# Esperar notificación
receive do
{:DOWN, ^ref, :process, ^pid, razon} ->
IO.puts("Proceso #{inspect(pid)} terminó: #{inspect(razon)}")
end
Registro de procesos
Puedes dar nombre a un proceso para encontrarlo fácilmente:
# Registrar un proceso con un nombre
pid = spawn(fn ->
receive do
msg -> IO.puts("Servidor recibió: #{msg}")
end
end)
Process.register(pid, :mi_servidor)
# Enviar mensaje por nombre
send(:mi_servidor, "Hola")
# Obtener PID por nombre
iex> Process.whereis(:mi_servidor)
#PID<0.115.0>
Task: Procesos para trabajo asíncrono
El módulo Task simplifica el trabajo con procesos para tareas únicas:
# Ejecutar tarea asíncrona
tarea = Task.async(fn ->
Process.sleep(1000) # Simular trabajo
42
end)
# Hacer otras cosas mientras...
IO.puts("Trabajando en paralelo...")
# Obtener resultado (espera si es necesario)
resultado = Task.await(tarea)
IO.puts("Resultado: #{resultado}")
Múltiples tareas en paralelo
# Ejecutar varias tareas concurrentemente
tareas = Enum.map(1..5, fn n ->
Task.async(fn ->
Process.sleep(100 * n) # Simular trabajo variable
n * n
end)
end)
# Esperar todas las tareas
resultados = Task.await_many(tareas)
# [1, 4, 9, 16, 25]
Agent: Estado simple
Agent es una abstracción simple para mantener estado:
# Iniciar un Agent
{:ok, agente} = Agent.start_link(fn -> 0 end)
# Obtener estado
Agent.get(agente, & &1)
# 0
# Actualizar estado
Agent.update(agente, &(&1 + 1))
# Obtener y actualizar en un paso
Agent.get_and_update(agente, fn estado ->
{estado, estado + 10} # {valor_retornado, nuevo_estado}
end)
# 1 (retorna el estado anterior)
Agent.get(agente, & &1)
# 11
Task: Para trabajo único asíncrono.
Agent: Para estado simple sin lógica compleja.
GenServer: Para servidores con estado y lógica compleja (siguiente capítulo).
Crea dos procesos que jueguen ping-pong:
- Un proceso envía
:pingy espera:pong - El otro responde
:pongcuando recibe:ping - Que intercambien 5 mensajes y luego terminen
Usa Task para calcular en paralelo:
- La suma de 1 a 1,000,000
- El factorial de 20
- Los primeros 30 números de Fibonacci
Mide cuánto tarda en paralelo vs secuencial.
Implementa una lista de tareas (TODO list) usando Agent:
agregar(agente, tarea)- agrega una tareacompletar(agente, indice)- marca como completadalistar(agente)- muestra todas las tareaspendientes(agente)- cuenta tareas pendientes