`SharedArrayBuffer` в TypeScript

`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-канал.

YouTube Video