`SharedArrayBuffer` в TypeScript
В этой статье рассматривается SharedArrayBuffer в TypeScript.
Мы объясним использование SharedArrayBuffer в TypeScript на практических примерах.
YouTube Video
SharedArrayBuffer в TypeScript
SharedArrayBuffer — это механизм для совместного использования одного и того же пространства памяти между несколькими потоками, например, Web Workers. В сочетании с Atomics можно управлять состояниями гонки данных и выполнять операции с разделяемой памятью с низкой задержкой.
Предварительные требования и замечания
При использовании SharedArrayBuffer в браузере необходимы заголовки COOP и COEP для соблюдения требований безопасности, известных как изоляция между источниками (cross-origin isolation). В Node.js работа с разделяемой памятью относительно проста при использовании worker_threads.
Основные концепции
SharedArrayBuffer — это объект, представляющий собой последовательность байтов фиксированной длины, и вы можете читать и записывать числа через TypedArray и подобные представления. Простые операции чтения/записи не синхронизированы, поэтому для обеспечения атомарных операций используется API Atomics, а для координации — механизмы wait и notify.
Простой счётчик (Браузерная версия)
В этом примере основной поток создаёт SharedArrayBuffer и передаёт его Web Worker, и оба потока инкрементируют общий счётчик. Это демонстрирует минимальный шаблон: атомарное сложение с помощью Atomics.add и чтение с помощью Atomics.load.
main.ts (Браузерная часть)
Этот пример показывает, как основной поток создаёт SharedArrayBuffer и разделяет его с worker для многопоточного доступа.
1// main.ts
2// Create a 4-byte (Int32) shared buffer for one counter
3const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 1);
4const counter = new Int32Array(sab); // view over the buffer
5
6// Create worker and transfer the SharedArrayBuffer
7const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
8worker.postMessage({ sab });
9
10// Increase counter in main thread every 200ms
11setInterval(() => {
12 const newVal = Atomics.add(counter, 0, 1) + 1; // Atomically increment
13 console.log('Main incremented ->', newVal);
14}, 200);
15
16// Listen for messages (optional)
17worker.onmessage = (e) => {
18 console.log('From worker:', e.data);
19};- В этом коде основной поток использует
Atomics.addдля атомарного увеличения значения. Со стороныworker.tsк тому жеSharedArrayBufferможно получить доступ и производить с ним операции.
worker.ts (Браузерный Worker)
Это пример, в котором воркер получает тот же разделяемый буфер и периодически уменьшает или изменяет его значения.
1// worker.ts
2self.onmessage = (ev: MessageEvent) => {
3 const { sab } = ev.data as { sab: SharedArrayBuffer };
4 const counter = new Int32Array(sab);
5
6 // Every 350ms, atomically add 2 (demonstration)
7 setInterval(() => {
8 const newVal = Atomics.add(counter, 0, 2) + 2;
9 // Optionally notify main thread (postMessage)
10 self.postMessage(`Worker added 2 -> ${newVal}`);
11 }, 350);
12};- Worker также работает с той же памятью через
Int32Array, а обновления производятся без состояния гонки благодаряAtomics.
Синхронизация с использованием wait/notify
С помощью Atomics.wait и Atomics.notify можно приостанавливать потоки до выполнения определённых условий, обеспечивая синхронизацию между потоками внутри воркеров на основе событий. В браузерах самым безопасным вариантом считается использование Atomics.wait внутри Worker.
producer.ts (Браузерный Worker-производитель)
Производитель записывает данные и уведомляет потребителя с помощью notify.
1// producer.ts (worker)
2self.onmessage = (ev: MessageEvent) => {
3 const { sab } = ev.data as { sab: SharedArrayBuffer };
4 const state = new Int32Array(sab); // state[0] will be 0=empty,1=filled
5
6 // produce every 500ms
7 setInterval(() => {
8 // produce: set state to 1 (filled)
9 Atomics.store(state, 0, 1);
10 Atomics.notify(state, 0, 1); // wake one waiter
11 self.postMessage('produced');
12 }, 500);
13};consumer.ts (Браузерный Worker-потребитель)
Потребитель ожидает с помощью Atomics.wait и возобновляет обработку после уведомления от производителя.
1// consumer.ts (worker)
2self.onmessage = (ev: MessageEvent) => {
3 const { sab } = ev.data as { sab: SharedArrayBuffer };
4 const state = new Int32Array(sab);
5
6 // consumer loop
7 (async function consumeLoop() {
8 while (true) {
9 // if state is 0 => wait until notified (value changes)
10 while (Atomics.load(state, 0) === 0) {
11 // Blocks this worker until notified or timeout
12 Atomics.wait(state, 0, 0);
13 }
14 // consume (reset to 0)
15 // (processing simulated)
16 // read data from another shared buffer in real scenarios
17 Atomics.store(state, 0, 0);
18 // continue loop
19 self.postMessage('consumed');
20 }
21 })();
22};- В этом шаблоне производитель уведомляет потребителя через
Atomics.notify, а потребитель эффективно ожидает с помощьюAtomics.wait.Atomics.waitнельзя вызывать в основном потоке браузера. Чтобы предотвратить зависание интерфейса пользователя, использованиеAtomics.waitограничено только Worker-ами.
Практический пример с Node.js (worker_threads)
В среде Node.js обработку возможностей SharedArrayBuffer можно реализовать с помощью worker_threads. Ниже приведён типизированный пример на TypeScript для Node.js.
main-node.ts
Главный поток создаёт буфер и передаёт его Worker-у.
1// main-node.ts
2import { Worker } from 'worker_threads';
3import path from 'path';
4
5// Create SharedArrayBuffer for one 32-bit integer
6const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
7const counter = new Int32Array(sab);
8
9// Spawn worker and pass sab via workerData
10const worker = new Worker(path.resolve(__dirname, 'worker-node.js'), {
11 workerData: { sab }
12});
13
14// Log messages from worker
15worker.on('message', (msg) => console.log('Worker:', msg));
16
17// Increment in main thread periodically
18setInterval(() => {
19 const val = Atomics.add(counter, 0, 1) + 1;
20 console.log('Main increment ->', val);
21}, 300);worker-node.ts
В этом примере на стороне Worker используются parentPort и workerData из worker_threads.
1// worker-node.ts
2import { parentPort, workerData } from 'worker_threads';
3
4const sab = workerData.sab as SharedArrayBuffer;
5const counter = new Int32Array(sab);
6
7// Periodically add 5
8setInterval(() => {
9 const val = Atomics.add(counter, 0, 5) + 5;
10 parentPort?.postMessage(`Added 5 -> ${val}`);
11}, 700);- В Node.js нет ограничений COOP и COEP как в браузерах, поэтому разделяемую память можно легко использовать просто через
worker_threads. При использовании TypeScript обратите внимание на то, используете ли вы сборку с настройками CommonJS или ESM.
Особенности типизации в TypeScript
SharedArrayBuffer и Atomics включены в стандартные определения типов DOM и библиотек, поэтому их можно использовать непосредственно в TypeScript. При обмене сообщениями с Worker-ами безопаснее определять интерфейсы и чётко указывать типы.
1// Example: typed message
2type WorkerMessage = { type: 'init'; sab: SharedArrayBuffer } | { type: 'ping' };- Явное определение типов делает обработку
postMessageиonmessageболее безопасной и позволяет использовать проверку типов.
Практические сценарии использования
SharedArrayBuffer необходим не всегда, но он крайне эффективен в ситуациях, когда требуются преимущества высокой скорости, обеспечиваемые общей памятью. Понимание ситуаций, в которых это эффективно, позволяет делать правильный выбор технологий.
- Подходит для обработки с низкой задержкой, требующей высокоскоростных общих буферов, может использоваться для обработки аудио/видео в реальном времени или физики в играх.
- Для простого обмена данными или передачи больших данных может быть проще использовать
Transferable ArrayBufferилиpostMessage, чемSharedArrayBuffer.
Ограничения и безопасность
Для использования SharedArrayBuffer в браузере требуется изоляция между источниками: установите COOP в same-origin-allow-popups и COEP в require-corp. SharedArrayBuffer будет отключен, если эти требования не выполнены.
Рекомендации по производительности и отладке
Атомарные операции (Atomics) выполняются быстро, но частые ожидания и избыточная синхронизация могут увеличить задержку.
Для безопасного и эффективного обращения с разделяемой памятью следует проверить следующие моменты:.
- Представления вроде
Int32Arrayследует использовать с правильным выравниванием по байтам. - Чётко определяйте, какие индексы одного и того же разделяемого буфера используются какими процессами, и поддерживайте согласованные соглашения в коде.
- Если обращаться с
SharedArrayBufferкак с обычнымArrayBuffer, возникнут состояния гонки данных. Запись в один и тот же индекс из нескольких потоков без использованияAtomicsопасна.
Резюме
Правильное использование SharedArrayBuffer и Atomics позволяет реализовать безопасные и быстрые операции с разделяемой памятью даже в TypeScript. Начать проще всего с простых шаблонов синхронизации, таких как счётчики или сигналы, а при тщательном управлении синхронизацией и индексами можно эффективно работать даже в сценариях с низкой задержкой.
Вы можете следовать этой статье, используя Visual Studio Code на нашем YouTube-канале. Пожалуйста, также посмотрите наш YouTube-канал.