Inicio / Elixir / Elixir: Programación Funcional y Concurrente / Procesos y Concurrencia

Procesos y Concurrencia

spawn, send/receive, links, monitors y Task.

Intermedio
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Procesos y Concurrencia en Elixir

La concurrencia es el corazón de Elixir. Los procesos de la BEAM no son procesos del sistema operativo, sino entidades extremadamente ligeras gestionadas por la máquina virtual. Un sistema Elixir puede ejecutar millones de procesos simultáneos, cada uno con su propia memoria y recolección de basura.

Spawn: Crear Procesos

La forma más básica de crear un proceso es con spawn/1:

# Spawn crea un proceso y retorna su PID
pid = spawn(fn ->
  IO.puts("¡Hola desde el proceso #{inspect(self())}!")
end)

# El proceso padre continúa inmediatamente
IO.puts("Proceso creado: #{inspect(pid)}")
Process.alive?(pid)  # => false (ya terminó)

# spawn_link: similar pero enlaza el proceso al padre
pid = spawn_link(fn ->
  Process.sleep(1000)
  IO.puts("Proceso terminado")
end)

Send y Receive: Comunicación entre Procesos

Los procesos se comunican mediante paso de mensajes asíncrono:

# Enviar y recibir mensajes
defmodule Mensajero do
  def escuchar do
    receive do
      {:saludo, nombre} ->
        IO.puts("¡Hola, #{nombre}!")
        escuchar()  # Seguir escuchando

      {:despedida, nombre} ->
        IO.puts("¡Adiós, #{nombre}!")
        escuchar()

      :detener ->
        IO.puts("Deteniendo el proceso...")
        # No llamamos escuchar(), el proceso termina
    after
      5000 ->
        IO.puts("Timeout: sin mensajes por 5 segundos")
    end
  end
end

pid = spawn(&Mensajero.escuchar/0)

send(pid, {:saludo, "Ana"})      # => ¡Hola, Ana!
send(pid, {:despedida, "Pedro"}) # => ¡Adiós, Pedro!
send(pid, :detener)              # => Deteniendo el proceso...

Patrón Request-Reply

Un patrón común es enviar un mensaje y esperar una respuesta:

defmodule Contador do
  def iniciar(valor_inicial \\ 0) do
    spawn(fn -> bucle(valor_inicial) end)
  end

  defp bucle(conteo) do
    receive do
      {:incrementar, caller} ->
        nuevo = conteo + 1
        send(caller, {:ok, nuevo})
        bucle(nuevo)

      {:obtener, caller} ->
        send(caller, {:ok, conteo})
        bucle(conteo)

      {:decrementar, caller} ->
        nuevo = conteo - 1
        send(caller, {:ok, nuevo})
        bucle(nuevo)
    end
  end

  # Funciones cliente
  def incrementar(pid) do
    send(pid, {:incrementar, self()})
    receive do
      {:ok, valor} -> valor
    end
  end

  def obtener(pid) do
    send(pid, {:obtener, self()})
    receive do
      {:ok, valor} -> valor
    end
  end
end

pid = Contador.iniciar(0)
Contador.incrementar(pid)  # => 1
Contador.incrementar(pid)  # => 2
Contador.obtener(pid)      # => 2

Links: Procesos Enlazados

Los links crean una relación bidireccional entre procesos. Si uno falla, el otro también:

# spawn_link enlaza automáticamente
pid = spawn_link(fn ->
  Process.sleep(1000)
  raise "¡Error en proceso hijo!"
end)
# El proceso padre también morirá después de 1 segundo

# Atrapar exits
Process.flag(:trap_exit, true)
pid = spawn_link(fn -> exit(:fallo) end)

receive do
  {:EXIT, ^pid, razon} ->
    IO.puts("Proceso #{inspect(pid)} terminó: #{inspect(razon)}")
end

Monitors: Supervisión Unidireccional

A diferencia de los links, los monitors son unidireccionales — el observador recibe una notificación sin ser afectado:

pid = spawn(fn ->
  Process.sleep(500)
  exit(:terminado)
end)

ref = Process.monitor(pid)

receive do
  {:DOWN, ^ref, :process, ^pid, razon} ->
    IO.puts("Proceso monitoreado terminó: #{inspect(razon)}")
end
# => Proceso monitoreado terminó: :terminado

El Módulo Process

El módulo Process proporciona funciones útiles para gestionar procesos:

# Información del proceso actual
self()                    # PID del proceso actual
Process.alive?(pid)       # ¿El proceso sigue vivo?

# Registrar un proceso con nombre
Process.register(pid, :mi_proceso)
send(:mi_proceso, :mensaje)

# Dormir el proceso
Process.sleep(1000)  # Pausa de 1 segundo

# Listar procesos
Process.list()           # Todos los procesos
Process.info(self())     # Info del proceso actual
Process.info(self(), :message_queue_len)  # Mensajes en cola

# Terminar un proceso
Process.exit(pid, :kill)  # Forzar terminación

Task: Concurrencia Simplificada

El módulo Task simplifica los patrones comunes de concurrencia:

# Ejecutar una tarea asíncrona y esperar resultado
tarea = Task.async(fn ->
  Process.sleep(1000)
  42
end)

resultado = Task.await(tarea)  # => 42

# Ejecutar múltiples tareas en paralelo
resultados =
  ["url1", "url2", "url3"]
  |> Enum.map(fn url ->
    Task.async(fn -> simular_peticion(url) end)
  end)
  |> Enum.map(&Task.await/1)

# Task.async_stream para procesamiento concurrente
1..10
|> Task.async_stream(fn n ->
  Process.sleep(100)
  n * n
end, max_concurrency: 4)
|> Enum.to_list()
# => [ok: 1, ok: 4, ok: 9, ...]

Resumen

Los procesos ligeros de la BEAM son la base del modelo de concurrencia en Elixir. Con spawn para crear procesos, send/receive para comunicación por mensajes, links para propagación de errores y monitors para supervisión unidireccional, Elixir proporciona todas las primitivas necesarias para construir sistemas concurrentes robustos. El módulo Task simplifica los patrones más comunes, permitiendo ejecutar y coordinar trabajo en paralelo de forma segura.

🔒

Ejercicio práctico disponible

Procesos y mensajería

Desbloquear ejercicios
// Procesos y mensajería
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes