В чём разница между Task.async/await и spawn?

В Elixir и платформе BEAM (виртуальной машине Erlang), и spawn/1, и Task.async/await используются для параллельного выполнения кода, но между ними есть принципиальные различия в подходе, удобстве, обработке ошибок и интеграции с остальными компонентами языка. Рассмотрим их подробно.

spawn/1 и spawn/3

Функция spawn создаёт новый легковесный процесс в BEAM. Этот процесс работает параллельно с основным, не блокирует его и существует независимо. Он не возвращает результат напрямую — только PID нового процесса.

Пример:

pid = spawn(fn -> IO.puts("Привет из другого процесса") end)

Если вы хотите передавать или получать сообщения — используется механизм send/receive.

Особенности spawn:

  • Возвращает pid — идентификатор процесса.

  • Процесс работает независимо и не возвращает значение вызывающему.

  • Нужно вручную реализовывать механизм получения ответа через send и receive.

  • Не обрабатывает исключения: если внутри поднялась ошибка, она просто завершит процесс.

  • Не возвращает стек-трейс или ошибку в вызывающий процесс.

  • Не имеет автоматических таймаутов или ожидания завершения.

Task.async/await

Модуль Task — это высокоуровневый интерфейс над spawn, созданный для упрощения работы с параллельными задачами. Он запускает процесс (или несколько) с возможностью удобно получить результат через await. Это более безопасный и идиоматичный способ обработки параллельности в Elixir.

Пример:

task = Task.async(fn -> 1 + 2 end)
result = Task.await(task)
IO.puts(result) # 3

Особенности Task.async/await:

  • Task.async/1 возвращает структуру %Task{pid: ..., ref: ..., owner: ...}.

  • Task.await/1 блокирует вызывающий процесс до получения результата или ошибки.

  • Если внутри задачи произошла ошибка, она будет проброшена в await, с возможностью отлова.

  • Использует monitor и send, но скрывает это от пользователя.

  • Поддерживает таймауты: Task.await(task, timeout).

Сравнение spawn и Task.async/await

Характеристика spawn Task.async/await
Возврат значения Нет (нужно использовать send) Да (await возвращает результат)
--- --- ---
Удобство Низкоуровневая Высокоуровневая
--- --- ---
Обработка ошибок Ошибки не пробрасываются Ошибки пробрасываются в await
--- --- ---
Контроль завершения Нет встроенного Есть через Task.await
--- --- ---
Отмена/таймаут Нужно реализовывать вручную await поддерживает таймаут
--- --- ---
Интеграция с OTP Не интегрирован Лучше совместим с супервизорами и GenServer
--- --- ---
Использование Простые асинхронные задачи Параллельные задачи с возвратом значения
--- --- ---

Когда использовать spawn

  • Когда вам нужен полный контроль над процессом и вы хотите реализовать обмен сообщениями вручную.

  • Когда вы создаёте процессы, которые будут жить долго (например, рабочие процессы, акторы, демоны).

  • Когда не требуется возвращать значение напрямую.

  • При разработке GenServer, Supervisor и других компонентов OTP, где spawn_link, spawn_monitor могут быть предпочтительнее.

Когда использовать Task.async/await

  • Когда нужно выполнить параллельную задачу и получить результат обратно.

  • Когда важна безопасная обработка ошибок.

  • Когда задача не должна работать долго (типичная "короткая" задача).

  • Когда вы хотите таймаут по выполнению задачи.

  • При работе с потоковыми операциями, API-запросами, обработкой файлов.

Вариации модуля Task

  • Task.start/1 — как async, но без await, просто запускает задачу.

  • Task.async_stream/3 — для обработки коллекций с параллельным выполнением и контролем ошибок/таймаутов.

  • Task.Supervisor — для запуска задач под контролем супервизора.

Как обрабатываются ошибки

\# spawn
pid = spawn(fn -> raise "ошибка" end)
\# ошибка не будет проброшена в основной поток
\# Task
task = Task.async(fn -> raise "ошибка" end)
Task.await(task)
\# вызовет исключение в основном потоке

Таким образом, Task даёт более надёжную и управляемую модель при необходимости работы с результатами и ошибками. spawn подходит, когда нужен фундаментальный процесс без необходимости возвращать значение.