В чём разница между 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 подходит, когда нужен фундаментальный процесс без необходимости возвращать значение.