Elixir: Task

Elixir y Task para ser fuerte con la concurrencia al construir Apps. Hablamos un poco de como se utiliza, que aspectos nos parecen importantes y como con algo tan fácil de usar como Task tus Apps pueden ser mejores.

Elixir: Task
Luke trying to lift the X wing.

Luke: «Mover piedras es una cosa, esto es totalmente diferente.»
Yoda: «No, no es diferente, solo es diferente en tú mente.»

Bueno, henos aquí de nuevo y como saben aquí nos gusta hacer referencia a Star Wars cada que les enseñamos algo de Elixir. Hoy hablaremos de algo tan poderoso como el entrenamiento que le dio Yoda al querido Luke para volverse uno con la fuerza. La concurrencia es cosa que a menudo viene bien en cualquier proyecto. En los Softwares actuales muchas veces resulta necesaria para brindar resultados óptimos; Por eso hoy toca Task pero primero ¿Qué es la concurrencia?
La concurrencia es la capacidad de un sistema para ejecutar múltiples tareas o procesos al mismo tiempo, mejorando la eficiencia y el rendimiento al permitir que diferentes operaciones se superpongan en el tiempo, en lugar de ejecutarse secuencialmente una tras otra.

Task es el módulo de elixir que proporciona herramientas para la gestion de estos procesos. Permite crear y gestionar procesos asincrónicos que son ejecutados en paralelo. Si pensará en un ejemplo probablemente mencionaría un software que crea boletos para compra de eventos, estos boletos se mandaban a crear de manera asincrónica ya que podían llegar a ser miles y la creación del evento no debería esperar a la terminación de una tarea tan larga.

¿Cómo usamos Task?

Uno de los usos mas comunes de Task es convertir código secuencial en código concurrente usando Task.async/1 mientras este proceso es iniciado async notifica al llamado. Task.Async es una técnica vital que permite ejecutar tareas concurrentes sin complicaciones. Piensa en Task.async como esa herramienta que permite saltar de una tarea a otra, gestionando multiples procesos de manera eficiente.

defmodule Force do
  def lift_x_wing() do
    task = Task.async(fn -> luke_use_the_force() end)
    IO.puts("Task creada con PID: #{inspect(task.pid)}")
    Task.await(task)
  end
  
  def luke_use_the_force() do
    # Luke usa la fuerza para levantar el xwing pero el no es tan fuerte
    # por lo que este proceso sera un poco tardado.
    # Usamos Process.sleep para que esta funcion tarde 20 segundos.
    Process.sleep(2000)
  end
end

# Hacemos uso del modulo que acabamos de hacer.
result = Force.lift_x_wing()
IO.puts("Resultado: #{result}")

En este ejemplo, Task.async se utiliza para crear una tarea que hace una pausa. La tarea se ejecuta en paralelo y Task.await se usa para esperar el resultado de la tarea.

Esperando Resultados con Task.await

Task.await es una función clave para recuperar el resultado de una tarea asincrónica. Espera a que la tarea termine y devuelve su resultado. Si la tarea no finaliza en el tiempo especificado (en milisegundos), se genera una excepción.

result = Task.await(task, 20_000)  # Espera hasta 20 segundos

Es importante manejar adecuadamente las excepciones para evitar que el programa se bloquee si una tarea tarda demasiado en completarse.

Ejecutando Múltiples Tareas Concurrentemente

Uno de los mayores beneficios de Task es la capacidad de ejecutar múltiples tareas en paralelo y luego recolectar sus resultados. Esto se logra combinando Task.async y Task.await.

defmodule Training do
  def run_tasks do
    tasks = [
      Task.async(fn -> training_the_force(1) end),
      Task.async(fn -> training_the_force(2) end),
      Task.async(fn -> training_the_force(3) end)
    ]

    results = tasks |> Enum.map(&Task.await(&1, 5000))
    IO.inspect(results)
  end

  defp training_the_force(n) do
    :timer.sleep(2000)  # Simulamos un entrenammiento
    "Resultado #{n}"
  end
end

Training.run_tasks()

En este ejemplo, Task.async se utiliza para lanzar tres tareas pesadas en paralelo. Luego, Enum.map y Task.await se combinan para esperar y recolectar los resultados de todas las tareas.

Monitoreando Procesos con Task

Elixir permite monitorear procesos utilizando Process.monitor, y Task no es una excepción. Esto es útil para supervisar el estado de las tareas y manejar casos en los que una tarea falle o termine inesperadamente, no necesariamente tienes que traer a un alienigena verde colgado en la espalda supervisando cada proceso (referencia a Yoda entrenando a Luke desde su espalda).

defmodule MasterYoda do
  def monitor_tasks do
    task = Task.async(fn -> risky_operation() end)
    ref = Process.monitor(task.pid)

    receive do
      {:DOWN, ^ref, :process, _pid, reason} ->
        IO.puts("La tarea falló con razón: #{inspect(reason)}")
    after
      5000 ->
        IO.puts("La tarea sigue en ejecución.")
    end
  end

  defp risky_operation do
    raise "Algo salió mal"
  end
end

MasterYoda.monitor_tasks()

En este ejemplo, Process.monitor se utiliza para supervisar una tarea que podría fallar. Si la tarea termina inesperadamente, se envía un mensaje :DOWN que se recibe y maneja adecuadamente.

Task.Supervisor: Supervisando Tareas

Para escenarios más complejos y robustos, Elixir proporciona Task.Supervisor, un módulo que facilita la supervisión y gestión de tareas. Esto es particularmente útil para aplicaciones que necesitan ejecutar y monitorear un gran número de tareas.

defmodule JediCouncil do
  def start_supervised_tasks do
    {:ok, supervisor} = Task.Supervisor.start_link(name: MyTaskSupervisor)

    Task.Supervisor.async_nolink(supervisor, fn -> war_defenses(1) end)
    Task.Supervisor.async_nolink(supervisor, fn -> war_defenses(2) end)
  end

  defp war_defenses(n) do
    :timer.sleep(3000)
    IO.puts("Guerra #{n} completada.")
  end
end

JediCouncil.start_supervised_tasks()

En este ejemplo, Task.Supervisor se utiliza para crear y supervisar dos tareas largas. async_nolink lanza las tareas sin vincularlas al proceso actual, permitiendo que sigan ejecutándose incluso si el proceso principal termina. Pensemos en este modulo como el consejo Jedi supervisando la guerra de los clones (¡ya sé! perdieron la guerra, pero pues si la supervisaron).

Conclusión

Task es una herramienta muy fuerte dentro de los módulos de elixir, flexible y manejable donde las aplicaciones son beneficiadas por la concurrencia. Ese paralelismo que nos permite supervisar y gestionar estos procesos brindan capacidades necesarias a la hora de construir aplicaciones eficientes y escalábles.

Al dominar Task, puedes aprovechar al máximo la naturaleza concurrente de Elixir, lo digo de nuevo piensa en Luke. Antes de ir con Yoda su uso de la fuerza era limitado, despues de Yoda (Task) su uso en la fuerza era fuerte, lleno de confianza, sé como Luke y aprende a dominar Task permitiendo que tu código sea tan ágil y poderoso como un el del Jedi más legendario del universo de Star Wars.

¡Que la Fuerza de la concurrencia esté contigo!

Referencias:

  1. Elixir Lang Documentation - Task
  2. Programming Elixir ≥ 1.6

Y recuerda, siempre puedes visitarnos para que hagamos tus Apps en:

Foxlabs Developers
We have built and launched our clients’ applications, guiding them from planning to production and scaling.