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-память на диск и восстанавливать при старте — акторы продолжат работу
- Class «натягивается» на SAB: конструктор принимает
- Цитата:
«Сотни гигабайтов повыделять на самом деле, и это не будет приводить к деградации сборщика мусора джаваскриптового, потому что это unmanaged память.»
- Тезисы: