Передача данных между потоками: SharedArrayBuffer и Transferable

Ammo.js, являясь портом библиотеки Bullet Physics на JavaScript с использованием Emscripten, работает внутри среды JavaScript с ограничениями однопоточности основной модели выполнения. Для увеличения производительности физического движка и уменьшения блокировок основного потока применяется Web Workers. Взаимодействие между основным потоком и воркерами требует эффективных механизмов передачи данных, особенно при работе с большим количеством объектов и коллизий.

Основные способы передачи данных в контексте Web Workers:

  • Transferable Objects — объекты, передаваемые между потоками без копирования, передача прав собственности.
  • SharedArrayBuffer (SAB) — область памяти, доступная одновременно нескольким потокам, поддерживающая атомарные операции.

SharedArrayBuffer: совместное использование памяти

SharedArrayBuffer предоставляет возможность разделения блока памяти между главным потоком и воркерами. При этом:

  • Доступ к данным осуществляется напрямую, без копирования, что критично для больших массивов чисел, например, позиций, скоростей или индексов треугольников физической сцены.
  • Требуется синхронизация при одновременном доступе для предотвращения гонок данных. Для этого применяются атомарные операции через объект Atomics.

Пример использования SharedArrayBuffer для хранения позиций объектов:

const numObjects = 1000;
const buffer = new SharedArrayBuffer(Float32Array.BYTES_PER_ELEMENT * numObjects * 3);
const positions = new Float32Array(buffer);

// Запись координат объекта
positions[0] = 1.0;
positions[1] = 2.0;
positions[2] = 3.0;

// Чтение координат в другом потоке
console.log(positions[0], positions[1], positions[2]);

Особенности:

  • Все изменения, внесённые в SharedArrayBuffer в одном потоке, моментально видны в другом.
  • Использование Atomics позволяет безопасно выполнять операции add, sub, load, store, compareExchange и wait/notify для синхронизации.

Transferable Objects: передача прав собственности

Transferable Objects позволяют перемещать объекты (например, ArrayBuffer) между потоками без копирования. После передачи объект в исходном потоке становится недоступным, что исключает расходы на дублирование памяти.

Пример передачи буфера с позицией объектов в воркер:

// В главном потоке
const buffer = new ArrayBuffer(Float32Array.BYTES_PER_ELEMENT * numObjects * 3);
worker.postMessage({ positions: buffer }, [buffer]);

// После передачи buffer больше нельзя использовать в главном потоке

Особенности:

  • Высокая производительность для больших массивов данных.
  • Не подходит для параллельного одновременного доступа: объект становится доступен только в целевом потоке.

Практическая интеграция Ammo.js

Ammo.js использует объекты btRigidBody, btTransform, btVector3 для моделирования физики. В многопоточной архитектуре необходимо передавать:

  • Позиции и ориентации объектов: массивы Float32Array.
  • Состояния коллизий: индексы столкнувшихся объектов и силы ударов.
  • Силы и импульсы: при применении внешних воздействий на объекты.

Пример использования SharedArrayBuffer для обмена данными между воркером и главным потоком:

// Инициализация SharedArrayBuffer для позиций и ориентаций
const numObjects = 500;
const buffer = new SharedArrayBuffer(Float32Array.BYTES_PER_ELEMENT * numObjects * 7);
const stateArray = new Float32Array(buffer); // x, y, z, qx, qy, qz, qw

// Отправка буфера воркеру
worker.postMessage({ state: buffer });

// В воркере
onmess age = function(e) {
  const state = new Float32Array(e.data.state);
  // Обновление позиций физики
  for (let i = 0; i < numObjects; i++) {
    const idx = i * 7;
    // пример применения новых координат в Ammo.js
    const transform = new Ammo.btTransform();
    transform.setOrigin(new Ammo.btVector3(state[idx], state[idx+1], state[idx+2]));
    transform.setRotation(new Ammo.btQuaternion(state[idx+3], state[idx+4], state[idx+5], state[idx+6]));
    // rigidBody.update(transform); // псевдокод
  }
};

Преимущество SharedArrayBuffer здесь в том, что обновления состояний происходят без копирования массивов, что особенно важно при большом числе объектов.

Сравнение подходов

Механизм Копирование данных Совместный доступ Подходит для Ограничения
Transferable Objects Нет (передача прав) Нет Однократная передача больших массивов После передачи исходный поток теряет доступ
SharedArrayBuffer Нет (разделение памяти) Да Параллельный доступ, синхронизация Требуется атомарная синхронизация, уязвимость к гонкам

Рекомендации по использованию в Ammo.js

  • Для обновления состояния большого количества объектов каждый кадр — SharedArrayBuffer обеспечивает минимальные накладные расходы.
  • Для разовой передачи больших массивов данных — Transferable Objects позволяют быстро передать массивы без копирования.
  • В многопоточной симуляции рекомендуется объединять оба подхода: SAB для постоянного обмена состоянием и Transferable для передачи больших блоков с начальной загрузкой или результатами длительных вычислений.
  • Обязательная проверка использования Atomics для синхронизации при параллельных записях, чтобы избежать повреждения данных и некорректной симуляции физических процессов.

Использование этих механизмов позволяет создавать высокопроизводительные физические симуляции в браузере с Ammo.js, эффективно распределяя нагрузку между потоками и минимизируя задержки обновления сцены.