Браузер и фронтенд-рантайм
SharedArrayBuffer, Atomics и cross-origin isolation
Каждый 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-потоков паркует незанятых воркеров и пробуждает их при появлении работы.
- Требует
- 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 отношения), которые делают предшествующие неатомарные записи видимыми. Практическое правило для прикладного кода:
- Воркер пишет bulk-данные обычными typed-array записями.
- Воркер делает один
Atomics.storeво флаг “данные готовы”. - Читатель делает один
Atomics.loadфлага. - Как только читатель увидел нужное значение — все 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), чтобы перечислить, что сломается, прежде чем включать принудительно.
> 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()` запрещён на главном потоке, но разрешён внутри воркера?
Страница деплоит COOP + COEP для разблокировки многопоточного WASM. После деплоя crossOriginIsolated = true, но WASM-модуль паникует при запуске с 'memory.grow failed'. Найдите причину.
Почему это работает
Зачем существует Spectre-гейт? Spectre — уязвимость микроархитектуры CPU (CVE-2017-5753): спекулятивное выполнение утекает память через process boundary через timing side-channels. Высокоточные таймеры (в том числе те, что можно построить из SharedArrayBuffer, инкрементируя счётчик в одном потоке и замеряя чтения в другом) делают эти атаки практичными в браузере. COOP не даёт вашей странице делить процесс с потенциально вредоносными кроссоригинными страницами; COEP не позволяет загружать кроссоригинные ресурсы без явного согласия. Вместе они дают браузеру уверенность, что можно включить высокоточную разделяемую память, не утекая секреты смежных origin’ов.
- 01Перечислите всё, что должно выполняться для доступности SharedArrayBuffer, и что это стоит.
- 02Почему атомарные операции необходимы при использовании SharedArrayBuffer?
- 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
- Federation и lookahead: батчинг за пределами DataLoadermiddle
- Senior GraphQL API: scheduling-контракт, изоляция арендаторов, наблюдаемостьsenior
- Лок и single-flight: ограничение параллельных rebuildmiddle
- Stale-while-revalidate и CDN request coalescingmiddle
- Детектирование stampede и дизайн TTL для продакшенаmiddle
- Метастабильный сбой, fencing-токены и production-постмортемыsenior
- Что такое отношение: таблицы, строки, ключи и ограниченияjunior
- Ограничения, ключи и типы данных Postgresmiddle
- JSONB, массивы и когда side table побеждаетmiddle
- Целостность схемы: deferral, версионирование и сбои в продакшнеsenior
- Где происходит data fetching — и почему это решает LCPjunior
- React Server Components и Suspense streamingmiddle
- Senior internals: RSC payload, слои кэша и production паденияsenior
- Конверт IPjunior
- Читаем IP-заголовокmiddle
- Что делает TLS и зачем он нуженjunior
- Расписание ключей, SNI, ALPN и расширенияsenior
- Защита 0-RTT, ECH, гибридный PQ и продакшн TLSsenior
- Двенадцать слоёв: один URL, семь действующих лицjunior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое OpenTelemetry: API, SDK, Collector, OTLPjunior
- Сигналы OTel, Semantic Conventions и проводной формат OTLPmiddle
- Collector OTel: receivers, processors, exporters и паттерны развёртыванияmiddle
- Vendor-нейтральность, eBPF-инструментирование, Operator и OTel в браузере и serverlesssenior
- Эксплуатация OTel Collector: надёжность, version skew, режимы отказа и управлениеsenior
- Что такое trace propagation и почему сломанная propagation хуже отсутствия трейсовjunior
- traceparent и tracestate: полный формат W3C-заголовкаmiddle
- Baggage и async-границы: перенос контекста через очереди и callback''''иmiddle
- Async context на разных языках, service mesh, миграция B3 и безопасностьsenior
- Production-сбои propagation, span links и платформенный дизайнsenior
- Debugging-воронка: SLO → RED → trace → profilejunior
- Архитектура OTel: один SDK, четыре сигнала, один wire-форматmiddle
- Петля инцидента: от пейджера до постмортема до предотвращенияmiddle
- Масштаб, безопасность и ROI наблюдаемых системsenior
- At-most-once, at-least-once, exactly-once: три контракта доставкиjunior
- Consumer-side dedup: самый дешёвый путь к exactly-once processingmiddle
- Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инцидентыsenior
- Что такое OAuth и почему пароли — не ответjunior
- Authorization code flow с PKCEmiddle
- Sender-constrained токены: DPoP и mTLSsenior
- OAuth в production: audience атаки, observability и реальные провалыsenior