Learning Book

SharedArrayBuffer и Atomics

Зачем общая память

postMessage копирует данные. Для большого массива чисел (миллионы элементов) это дорого — и по времени, и по памяти. SharedArrayBuffer (SAB) позволяет нескольким потокам читать и писать одну область памяти без копирования.

// Main thread
const sab = new SharedArrayBuffer(1024) // 1 KB общей памяти
const view = new Int32Array(sab)        // типизированный доступ

worker.postMessage({ buffer: sab })     // SAB НЕ копируется — это та же память
view[0] = 42

// Worker thread
self.onmessage = (e) => {
  const view = new Int32Array(e.data.buffer)
  console.log(view[0]) // 42 — та же память
}

Два потока, один буфер. Изменения видны обоим.

Data Race

Общая память = гонки данных. Два потока могут одновременно читать и писать одну ячейку:

// ❌ Data race: результат непредсказуем
// Main thread         // Worker
view[0] = 0
                       // view[0]++ (read 0, add 1, write 1)
view[0]++              // (read 0, add 1, write 1) ← потеряли инкремент воркера
// Ожидали: 2, получили: 1

Операция view[0]++ — это три шага: чтение, сложение, запись. Если два потока выполняют их одновременно, один результат перезаписывает другой. Это гонка данных (data race).

Atomics

Atomics — встроенный объект с атомарными операциями: гарантированно неделимые чтение-модификация-запись.

Базовые операции

const sab = new SharedArrayBuffer(4)
const view = new Int32Array(sab) // один Int32 элемент

// Атомарная запись и чтение
Atomics.store(view, 0, 42)        // view[0] = 42 (атомарно)
Atomics.load(view, 0)             // 42 (атомарно)

// Атомарный инкремент
Atomics.add(view, 0, 1)           // view[0] += 1 (атомарно), возвращает старое значение
Atomics.sub(view, 0, 1)           // view[0] -= 1 (атомарно)

// Битовые операции
Atomics.and(view, 0, 0xFF)        // view[0] &= 0xFF
Atomics.or(view, 0, 0x01)         // view[0] |= 0x01
Atomics.xor(view, 0, 0x01)        // view[0] ^= 0x01

// Обмен
Atomics.exchange(view, 0, 100)    // view[0] = 100, возвращает старое значение

Compare-and-Exchange (CAS)

Самая мощная атомарная операция — основа lock-free алгоритмов:

// Записать newValue, только если текущее значение === expected
const old = Atomics.compareExchange(view, 0, expected, newValue)
// old === expected → запись произошла
// old !== expected → ничего не произошло, кто-то изменил значение раньше

CAS-цикл — типичный паттерн:

function atomicIncrement(view, index) {
  let old
  do {
    old = Atomics.load(view, index)
  } while (Atomics.compareExchange(view, index, old, old + 1) !== old)
  return old + 1
}

Безопасный инкремент

// ❌ Гонка данных
view[0]++ // read → modify → write (3 шага, не атомарно)

// ✅ Атомарно
Atomics.add(view, 0, 1) // один неделимый шаг

Wait и Notify

Механизм ожидания и уведомления — аналог condition variable:

// Worker (ждёт)
const result = Atomics.wait(view, 0, 0)
// Поток спит, пока view[0] === 0
// result: 'ok' (разбудили), 'not-equal' (значение уже не 0), 'timed-out'

// Main thread (будит)
Atomics.store(view, 0, 1)
Atomics.notify(view, 0, 1) // разбудить 1 ждущий поток
// Atomics.notify(view, 0, Infinity) — разбудить всех

Важно: Atomics.wait() блокирует поток. В main thread браузера его вызывать нельзя — выбросит ошибку. Только в воркерах. В Node.js — можно где угодно.

Пример: producer-consumer

// Общая память
const sab = new SharedArrayBuffer(8) // 2 × Int32
const flag = new Int32Array(sab, 0, 1)   // flag[0]: 0 = пусто, 1 = готово
const data = new Int32Array(sab, 4, 1)   // data[0]: значение

// Consumer (worker) — ждёт данные
while (true) {
  Atomics.wait(flag, 0, 0) // спим, пока flag === 0
  const value = Atomics.load(data, 0)
  console.log('Получил:', value)
  Atomics.store(flag, 0, 0) // сброс флага
}

// Producer (main) — отправляет данные
Atomics.store(data, 0, 42)
Atomics.store(flag, 0, 1)  // помечаем: данные готовы
Atomics.notify(flag, 0, 1) // будим consumer

COOP/COEP: заголовки безопасности

После уязвимости Spectre (2018) браузеры отключили SharedArrayBuffer по умолчанию. Чтобы его использовать, сервер должен отправлять заголовки:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
// Проверка в коде
if (typeof SharedArrayBuffer !== 'undefined') {
  // SAB доступен
} else {
  // Нужны COOP/COEP заголовки
}
DeepDive: Spectre и почему это важно

Spectre — атака по побочному каналу (side-channel), позволяющая через точные замеры времени читать память другого процесса. SharedArrayBuffer давал таймер высокого разрешения (через счётчик в отдельном потоке), что делало атаку практичной.

COOP/COEP создают изолированный контекст (cross-origin isolated), гарантирующий, что в процесс не загрузятся ресурсы из других origins. Это делает Spectre-атаки через SharedArrayBuffer бесполезными.

// Проверка cross-origin isolation
self.crossOriginIsolated // true → SAB доступен

Когда использовать SAB

СценарийpostMessageSharedArrayBuffer
Передать объект✅ Structured clone❌ Только числа
Большой числовой массив❌ Копирование✅ Без копирования
Несколько воркеров, общие данные❌ N копий✅ Одна копия
Синхронизация потоков❌ Нет механизма✅ Atomics.wait/notify
Высокая частота обновлений❌ Дорогой clone✅ Прямой доступ

Правило: если работаешь с числовыми данными и нужна скорость — SAB. Для всего остального — postMessage.

Итого

ФактОписание
SharedArrayBufferОбщая память между потоками, без копирования
Data raceОдновременная запись без синхронизации → непредсказуемый результат
AtomicsНеделимые операции: add, store, load, compareExchange
CASCompare-and-exchange — основа lock-free алгоритмов
wait/notifyБлокирующее ожидание + уведомление (только в воркерах)
COOP/COEPОбязательные заголовки для SAB в браузере (из-за Spectre)