Процессы и модель акторов

Вот ради чего выбирают Elixir. Процессы BEAM — это не потоки ОС: они невероятно лёгкие, изолированы и общаются только сообщениями. Это модель акторов в чистом виде.

Хочешь конкурентность? Создай процесс. Их можно иметь миллионы, и падение одного не тронет остальных.

Процесс создаётся функцией spawn, которая возвращает идентификатор (PID):

pid = spawn(fn ->
  IO.puts("Привет из процесса #{inspect(self())}")
end)

is_pid(pid)   # => true

Каждый процесс полностью изолирован: своя память, свой сборщик мусора. Они не делят состояние — только обмениваются сообщениями. Поэтому нет гонок и мьютексов.

# Миллион процессов — обыденность для BEAM
pids = for _ <- 1..1_000_000, do: spawn(fn -> :timer.sleep(100) end)
length(pids)   # => 1000000

Как работает под капотом (BEAM)

Процесс BEAM — не поток операционной системы. Это структура внутри VM, которая стартует с крошечной кучей (порядка сотен слов). Планировщик BEAM (по одному на ядро CPU) раскладывает тысячи процессов по немногим OS-потокам и вытесняюще переключает их после порции работы (редукций). Поэтому один «зависший» процесс не блокирует остальных, а создание процесса в тысячи раз дешевле, чем поток ОС. Изоляция памяти означает, что сбой одного процесса не повреждает данные других — фундамент отказоустойчивости.

  Модель акторов:

   [Процесс A] --сообщение--> [mailbox]
                                  |
                              [Процесс B]
   Нет общей памяти. Только асинхронные сообщения.
   Падение A не трогает B.

Та же идея на Python ▶

В Python потоки делят память, поэтому модель акторов имитируют изоляцией через очередь сообщений.

from queue import Queue

# Каждый "актор" — своё СОБСТВЕННОЕ состояние + входящая очередь (mailbox)
class Actor:
    def __init__(self, name):
        self.name = name
        self.mailbox = Queue()      # как mailbox процесса BEAM
        self.state = {}             # изолированное состояние

    def send(self, message):        # асинхронная отправка
        self.mailbox.put(message)

    def process(self):              # обработка по одному сообщению
        while not self.mailbox.empty():
            msg = self.mailbox.get()
            print(f"{self.name} получил: {msg}")

a = Actor("A")
a.send("привет")
a.send("ещё одно")
a.process()
# A получил: привет
# A получил: ещё одно

Частые ошибки

  • Сравнивать процессы с потоками ОС. Они в тысячи раз легче; «слишком много процессов» — почти всегда не проблема.
  • Искать общую память. Её нет; обмен только сообщениями. Попытка «расшарить состояние» — антипаттерн.
  • Не следить за «осиротевшими» spawn. Голый spawn без супервизии может тихо упасть; для долгоживущих процессов нужен OTP.

Best practices

  • Используйте процессы для конкурентности и изоляции, а не для «ускорения» чистых вычислений.
  • Один процесс — одна ответственность (один пользователь, одно соединение, одна сессия).
  • Для долгоживущих процессов берите абстракции OTP (Task, GenServer), а не голый spawn.

Итог. Процессы BEAM дают дешёвую, изолированную конкурентность по модели акторов. Но чтобы они приносили пользу, нужно научить их обмениваться сообщениями — этим займёмся в следующем уроке.

Процесс как единица concurrency и изоляции

Важно усвоить, для чего процессы нужны, а для чего — нет. Они блестящи как единица конкурентности (тысячи независимых задач параллельно) и изоляции (сбой одной не трогает другие). Но они не ускоряют сами по себе чистое вычисление: разбить один тяжёлый расчёт на процессы имеет смысл лишь тогда, когда части независимы и могут лечь на разные ядра. Типичные «правильные» процессы — это сущности предметной области: пользователь чата, игровая комната, подключённое устройство, фоновая задача.

Отдельно стоит знать про абстракцию Task — самый лёгкий способ запустить параллельную работу. Task.async стартует процесс, считающий значение, а Task.await дожидается результата. Для «сделай эти три независимых запроса одновременно» это идеальный инструмент: код выглядит почти последовательным, но выполняется параллельно. Task — это первая ступенька к OTP: вы получаете удобство и надзор, не погружаясь сразу в GenServer. С него часто и начинают приручать конкурентность на практике.

Проверьте себя
1. Чем процесс BEAM отличается от потока операционной системы?
AНичем
BПроцесс BEAM — лёгкая структура внутри VM с изолированной памятью; их можно иметь миллионы, и они в тысячи раз дешевле потоков ОС
CПроцесс BEAM медленнее
DПроцессы BEAM делят общую память
2. Как процессы BEAM обмениваются данными?
AЧерез общую память с мьютексами
BТолько асинхронными сообщениями — общей памяти нет
CЧерез глобальные переменные
DЧерез файлы на диске