SharedArrayBuffer и Atomics

Разделяемая память между потоками Node.js (worker_threads) без копирования + примитивы атомарных операций для синхронизации. Основа многопоточного программирования в JS.

Что это

ArrayBuffer живёт в managed-памяти одного V8-рантайма. SharedArrayBuffer (SAB) — это сырая unmanaged память вне V8-кучи, к которой могут обращаться несколько worker threads одновременно. Не сериализуется при передаче — пробрасывается по ссылке.

С SAB однопоточные приёмы JS ломаются: два потока могут одновременно писать в одну ячейку, один может прочитать разорванные 4 байта (2 от старого числа, 2 от нового). Поэтому есть Atomics — namespace с неделимыми операциями.

SharedArrayBuffer

// 1 КБ разделяемой памяти
const sab = new SharedArrayBuffer(1024);

// Доступ через типизированный массив
const i32 = new Int32Array(sab); // 256 элементов по 4 байта

// Передача воркеру — по ссылке, без копирования
const { Worker } = require('node:worker_threads');
new Worker('./worker.js', { workerData: { buffer: sab } });

// В worker.js:
// const { workerData } = require('node:worker_threads');
// const i32 = new Int32Array(workerData.buffer); // та же память

Преимущество перед обычным буфером: можно держать сотни гигабайт без деградации GC — это память вне V8 heap.

Atomics — атомарные операции

Atomics.load(arr, i);              // прочитать i-й элемент атомарно
Atomics.store(arr, i, value);      // записать и вернуть прежнее
Atomics.add(arr, i, n);            // arr[i] += n, вернуть прежнее
Atomics.sub(arr, i, n);            // arr[i] -= n
Atomics.and / or / xor             // битовые
Atomics.compareExchange(arr, i, expected, value); // CAS
Atomics.exchange(arr, i, value);   // безусловная атомарная замена
Atomics.wait(arr, i, value, timeout);  // блокирующее ожидание изменения
Atomics.notify(arr, i, count);     // разбудить count ожидающих

Все Atomics-операции делают одну логическую операцию неделимо: чтение, изменение, запись — между ними ничего не происходит.

Atomics.load() для Int32Array читает все 4 байта атомарно (защита от разорванного чтения).

Хранение объектов в SharedArrayBuffer

Идея: «натянуть» класс на сырую память. Точка = 8 байт (x, y как int32).

class Point {
  constructor(view, offset = 0) {
    this.view = view;       // Int32Array поверх SAB
    this.offset = offset;   // смещение в элементах int32
  }
  get x { return Atomics.load(this.view, this.offset); }
  get y { return Atomics.load(this.view, this.offset + 1); }
  move(dx, dy) {
    Atomics.add(this.view, this.offset, dx);
    Atomics.add(this.view, this.offset + 1, dy);
  }
  clone(targetView, targetOffset) {
    // Копируем 8 байт за раз через view.set
    targetView.set(this.view.subarray(this.offset, this.offset + 2), targetOffset);
  }
}

// Один большой буфер на массив структур
const sab = new SharedArrayBuffer(100 * 1024 * 1024); // 100 МБ
const view = new Int32Array(sab);
const p1 = new Point(view, 0);
const p2 = new Point(view, 2);

Зачем: для геймдева, физических симуляций, систем маршрутизации (миллионы узлов графа). Обычные JS-объекты не влезли бы в кучу + давление на GC.

Подводные камни

  • Гонки данных: без Atomics два потока пишут в одну ячейку — итог непредсказуем
  • Atomics не отменяет логические гонки: «если ячейка пуста — записать» нужно делать через compareExchange, иначе TOCTOU
  • Нет приватных полей: всё состояние — в сыром буфере, JS-поля класса теряются (новый инстанс может быть создан в любом потоке)
  • Atomics.wait блокирует поток: в main thread запрещено (бросает ошибку), только в воркерах. В main используется Atomics.waitAsync (если поддерживается)
  • SAB не работает в браузере без COOP/COEP headers (Spectre mitigations)
  • console.dir — тоже критическая секция: сериализует объект итерированием по свойствам, на shared memory может видеть промежуточное состояние

🎓 Источники

  • 🎓 Atomics, SharedArrayBuffer, worker_threads в Node.js · 2019-02-21

    • Тезисы:
      • SAB живёт между потоками, обычный ArrayBuffer — только в одном V8
      • Atomics.store возвращает старое значение → можно одной операцией атомарно записать и прочитать что было
      • SIMD-команды появлялись в JS, потом были удалены; Atomics — основной примитив синхронизации
      • Read tearing: 2 байта от одного числа, 2 от другого; Atomics.load() защищает
      • В однопоточном цикле инкремент arr[0]++ атомарен (одна операция event loop); в многопоточности — НЕТ
      • console.dir сериализует через итерирование — на shared memory нужно брать в критическую секцию
    • Цитата:

      «В single-thread цикл был неделим. В многопоточности гарантии нет.»

  • 🎓 Хранение состояния объекта в SharedArrayBuffer и использование Atomics · 2025-08-01

    • Тезисы:
      • Class «натягивается» на SAB: конструктор принимает view и offset, поля хранятся в сыром буфере
      • Int32Array индексирует по элементам, индекс 1 = 4 байта от начала
      • Один большой кусок SAB (100 МБ+) под массив структур: внутри сами раскладываем Point/Line/Shape по offset-ам
      • SharedArrayBuffer держит сотни гигабайт без деградации JS GC — это unmanaged память вне V8 heap
      • Реальный кейс: вычисление оптимальных маршрутов авиаперевозок — миллионы узлов, обычные объекты не помещаются
      • Object pool: данные в сырой памяти, обёртка-объект создаётся только при доступе и возвращается в пул
      • Можно дампить всю shared-память на диск и восстанавливать при старте — акторы продолжат работу
    • Цитата:

      «Сотни гигабайтов повыделять на самом деле, и это не будет приводить к деградации сборщика мусора джаваскриптового, потому что это unmanaged память.»

См. также