awesome-everything EN
↑ Обратно к восхождению

Браузер и фронтенд-рантайм

SharedArrayBuffer, Atomics и cross-origin isolation

Суть SAB — реальная разделяемая память между потоками, закрытая за COOP + COEP; Atomics предотвращает гонки данных; relaxed memory model означает, что атомарные операции — единственная надёжная гарантия порядка.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 18 min

Каждый postMessage либо копирует данные, либо передаёт владение. Но многопоточному WASM нужен разделяемый ring buffer, который и потоки кодека, и JS могут читать и писать одновременно — без копии, без передачи, просто общая память. И затем он молча возвращает undefined в продакшене.

Что такое SharedArrayBuffer

SharedArrayBuffer (SAB) — это блок памяти, который два и более потока могут читать и писать одновременно без копирования. Над ним создаются typed-array views на каждом потоке — и они видят одни и те же байты:

// главный поток
const sab = new SharedArrayBuffer(1024);
worker.postMessage({ sab }); // НЕ передаётся — именно разделяется

// воркер
self.onmessage = e => {
  const view = new Int32Array(e.data.sab);
  view[0] = 42; // сразу видно главному потоку
};

Это фундамент, на котором работает многопоточный WebAssembly в браузере — линейная память WASM является SharedArrayBuffer при компиляции с флагом -pthread.

Growable SAB

В современных браузерах SAB можно создать с возможностью роста:

const sab = new SharedArrayBuffer(4096, { maxByteLength: 64 * 1024 });
// later, on any thread:
sab.grow(8192); // расширить до 8 КБ

Это критично для WASM-рантаймов: линейная память WASM использует memory.grow, который под капотом вызывает grow() на SAB. Каждый рост аннулирует все существующие typed-array views — любой поток, удерживающий старый view и обращающийся к нему после роста, получает устаревшие данные. WASM-тулчейн должен эмитировать атомарные барьеры вокруг вызовов роста; кастомные аллокаторы, вызывающие grow() без синхронизации, порождают трудноуловимые краши.

Зачем нужен Atomics

Два потока, пишущие в одно место SAB без координации — это data race: результат зависит от таймингов и не определён. Atomics предоставляет операции, которые CPU гарантирует как неделимые:

  • Atomics.add(view, i, 1) — читает, прибавляет, записывает как один непрерываемый шаг. Два потока, инкрементирующие один счётчик, никогда не потеряют обновление.
  • Atomics.compareExchange(view, i, expected, replacement) — строительный блок lock-free структур данных.
  • Atomics.wait(view, i, expected)блокирует вызывающий поток до вызова Atomics.notify(view, i) из другого потока. Настоящий blocking primitive — запрещён на главном потоке, разрешён внутри воркеров. Пара wait/notify — это то, как пул WASM-потоков паркует незанятых воркеров и пробуждает их при появлении работы.
SharedArrayBuffer с первого взгляда
Требует
COOP: same-origin + COEP: require-corp
Проверка crossOriginIsolated
self.crossOriginIsolated === true
Без заголовков
typeof SharedArrayBuffer === 'undefined'
Atomics.wait
Запрещён на главном потоке — блокирует поток
Growable SAB
new SharedArrayBuffer(size, { maxByteLength })

Relaxed memory model

SAB открывает доступ к relaxed memory model: без атомиков запись одного потока не гарантированно видна другому в каком-либо определённом порядке — CPU и компилятор могут переупорядочивать неатомарные операции.

Атомарные операции устанавливают рёбра синхронизации (happens-before отношения), которые делают предшествующие неатомарные записи видимыми. Практическое правило для прикладного кода:

  1. Воркер пишет bulk-данные обычными typed-array записями.
  2. Воркер делает один Atomics.store во флаг “данные готовы”.
  3. Читатель делает один Atomics.load флага.
  4. Как только читатель увидел нужное значение — все bulk-записи гарантированно видны.

Lock-free структуры, написанные без этой дисциплины, дают баги, проявляющиеся только под нагрузкой и только на определённых CPU.

Гейт COOP/COEP

После Spectre (уязвимость CPU, 2018 год) высокоточная разделяемая память стала угрозой безопасности — SAB-backed таймер достаточно точен для side-channel атаки. Браузеры заперли SAB за двумя заголовками ответа, которые должен отдавать документ:

  • Cross-Origin-Opener-Policy: same-origin (COOP) — изолирует вашу browsing-context group от других origin’ов.
  • Cross-Origin-Embedder-Policy: require-corp (COEP) — требует, чтобы каждый кроссоригинный подресурс явно согласился на встраивание через Cross-Origin-Resource-Policy.

При наличии обоих self.crossOriginIsolated равен true и SharedArrayBuffer доступен. Без них — SharedArrayBuffer равен undefined и любой код, нуждающийся в нём — прежде всего многопоточный WASM — молча падает.

Цена: COEP ломает кроссоригинные изображения, шрифты и скрипты, не отправляющие совпадающий заголовок Cross-Origin-Resource-Policy. Включение изоляции означает аудит и починку каждого стороннего ресурса. Сначала включайте COEP в report-only режиме (Cross-Origin-Embedder-Policy-Report-Only), чтобы перечислить, что сломается, прежде чем включать принудительно.

Найди ошибку
log
> typeof SharedArrayBuffer
'undefined'

> self.crossOriginIsolated
false

> performance.getEntriesByType('navigation')[0].responseHeaders
// Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: (не установлен)

[WASM] запрошено потоков: 8
[WASM] SharedArrayBuffer недоступен — откат к однопоточному режиму
[WASM] инициализация завершена (однопоточный, в 4.2x медленнее цели)

Многопоточный WASM-модуль молча работает однопоточно в продакшене. Консоль показывает, что SharedArrayBuffer undefined, crossOriginIsolated = false. COOP установлен, COEP — нет. Что именно нужно исправить и что это сломает?

Викторина

Почему `Atomics.wait()` запрещён на главном потоке, но разрешён внутри воркера?

Проследи
1/4

Страница деплоит COOP + COEP для разблокировки многопоточного WASM. После деплоя crossOriginIsolated = true, но WASM-модуль паникует при запуске с 'memory.grow failed'. Найдите причину.

1
Step 1 of 4
WASM-модуль использует growable SharedArrayBuffer (опция maxByteLength), но браузер его не поддерживает
2
Locked
WASM-рантайм вызвал memory.grow без атомарной координации — другой поток был в середине чтения старой области памяти
3
Locked
Размер SharedArrayBuffer превышает лимиты браузера
4
Locked
COEP заблокировал один из WASM-импортов
Почему это работает

Зачем существует Spectre-гейт? Spectre — уязвимость микроархитектуры CPU (CVE-2017-5753): спекулятивное выполнение утекает память через process boundary через timing side-channels. Высокоточные таймеры (в том числе те, что можно построить из SharedArrayBuffer, инкрементируя счётчик в одном потоке и замеряя чтения в другом) делают эти атаки практичными в браузере. COOP не даёт вашей странице делить процесс с потенциально вредоносными кроссоригинными страницами; COEP не позволяет загружать кроссоригинные ресурсы без явного согласия. Вместе они дают браузеру уверенность, что можно включить высокоточную разделяемую память, не утекая секреты смежных origin’ов.

Вспомните перед уходом
  1. 01
    Перечислите всё, что должно выполняться для доступности SharedArrayBuffer, и что это стоит.
  2. 02
    Почему атомарные операции необходимы при использовании SharedArrayBuffer?
  3. 03
    Какой практический паттерн использовать для сигнализации 'данные готовы' через атомарный флаг в SharedArrayBuffer?
Итог

SharedArrayBuffer — это побег из message-passing: блок памяти, видимый всем потокам, удерживающим его view, с нулевыми накладными расходами на копирование. Это фундамент многопоточного WASM в браузере. Закрыт за COOP: same-origin и COEP: require-corp (или credentialless) — без обоих заголовков SharedArrayBuffer равен undefined и многопоточный WASM молча деградирует. Growable SAB (maxByteLength) позволяет расти линейной памяти WASM, но рост аннулирует все view — координация через атомики обязательна. Atomics предотвращает гонки данных: Atomics.add, Atomics.compareExchange и Atomics.wait/notify — примитивы. Atomics.wait запрещён на главном потоке, потому что блокирует — а блокировка главного потока замораживает страницу. Relaxed memory model означает: только атомарные операции гарантируют порядок между потоками.

Связанные уроки
встречается в41
Продолжить восхождение ↑Граничные случаи service worker: version skew, долговременность и ловушка навигации
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.