Actor Compose — композиция актора и состояния
Модель актора в JS: приватное состояние, очередь сообщений, методы отрабатываются один за другим. Композиция актора достигается через очередь, send/process API и сериализацию вызовов. Решает проблему shared state при async-разрывах.
Модель акторов = распределённый ООП
Если объекты находятся в разных процессах или вообще на разных машинах, и шлют друг другу сообщения, они не лезут друг к другу в память. Нет общих данных — нет race conditions.
Очереди решают масштабирование: 100 инстансов одного актора, 2 — другого. Round-robin по воркерам и тредам.
Проблема async-разрыва состояния
async move(dx, dy) {
this.x += dx; // до await
await save(this.x); // разрыв — кто-то может вызвать другой метод
this.y += dy; // после await — состояние могли изменить
}
Внутри метода бывают await-ы. До await изменяется состояние, и после await изменяется состояние. Состояние объекта меняется на фрагменте кода, который может быть разорван в асинхронность.
Это источник трудноуловимых багов в async-классах.
Защита через примитивы синхронизации
| Примитив | Назначение |
|---|---|
| Флаг | Примитивная и плохая защита |
| Блокировка с атомиками | Низкоуровневый lock |
| Семафор | Ограничение конкурентности |
| Mutex | Эксклюзивный доступ |
| Critical section | Защищённый блок |
| Spinlock | Активное ожидание |
В Node.js без worker_threads + SharedArrayBuffer этого почти нет. Решение — actor-модель и очередь.
Изоляция + закон Деметры
Это изоляция внутреннего state, создание внешнего контракта. Применение закона Деметры (don't talk to strangers) — никакие методы нельзя вызывать снаружи.
Прямой доступ к полям и методам — запрещён. Только через send/dispatch.
Методы актора сериализуются
Методы одного актора в этой модели могут вызываться только один за другим. Один вызван — он должен закончиться, тогда другой можно вызывать.
Гарантия: внутри метода state не меняется снаружи. Бинго — детерминированность.
Структура актора
class Point {
#x; #y;
#queue = ;
#processing = false;
#move(dx, dy) { this.#x += dx; this.#y += dy; }
#clone { return new Point(this.#x, this.#y); }
async send(message) {
return new Promise((resolve, reject) => {
this.#queue.push({ message, resolve, reject });
this.#process;
});
}
async #process {
if (this.#processing) return;
this.#processing = true;
while (this.#queue.length) {
const { message, resolve, reject } = this.#queue.shift();
try {
const result = await this.#dispatch(message);
resolve(result);
} catch (e) {
reject(e);
}
}
this.#processing = false;
}
async #dispatch({ type, args }) {
switch (type) {
case 'move': return this.#move(...args);
case 'clone': return this.#clone;
}
}
}
Нейминг send-метода
Receive — плохое имя. Варианты: dispatch, enqueue, post. Семантика: положить сообщение в актор.
actor.post({ type: 'move', args: [5, 10] }) — отчётливее, чем actor.move(5, 10).
Что хранить в очереди
В очередь записываем сообщение и добавляем resolve. Лучше добавить ещё reject. Может быть тогда объединить — чтобы был не распакованный message, а так и остался запакованным.
this.#queue.push({ message, resolve, reject });
Или храним сам Promise:
this.#queue.push({ message, promise: { resolve, reject } });
Преимущества
- Нет race conditions внутри одного актора
- Прозрачное масштабирование (актор в воркере, на другой машине)
- Backpressure через ограничение размера очереди
- Тестируемость — можно прокатить сообщения через mock-actor
- Чёткая граница "снаружи / внутри"
Связь с Erlang/Akka
Erlang VM — production-grade actor model. Akka на JVM. В Node.js — comlink, threads.js или ручная реализация.
Связанные темы
- Асинхронная композиция функций
- Парадигмы программирования (ООП vs ФП)
- Реактивное программирование основы
- Reactor pattern
Ресурсы
- Лекция: H_vc18rRyzA