SharedArrayBuffer в JavaScript

SharedArrayBuffer в JavaScript

В этой статье объясняется SharedArrayBuffer в JavaScript.

Мы подробно объясним основы SharedArrayBuffer, как использовать его, конкретные примеры применения и аспекты безопасности.

YouTube Video

SharedArrayBuffer в JavaScript

SharedArrayBuffer — это мощный инструмент в JavaScript для совместного использования памяти между потоками. Особенно в сочетании с Web Workers, он позволяет выполнять параллельную обработку, делая его эффективным для задач, требующих больших вычислений, и приложений с требованиями к работе в реальном времени.

Что такое SharedArrayBuffer?

SharedArrayBuffer предоставляет буфер памяти в JavaScript, который позволяет совместно использовать бинарные данные между потоками (в основном, с Web Workers). Обычный ArrayBuffer требует копирования данных между основным потоком и рабочими потоками, но SharedArrayBuffer позволяет непосредственно совместно использовать память без копирования, что существенно повышает производительность.

Функции

  • Общая память Это позволяет нескольким потокам работать с одной и той же областью памяти.
  • Улучшение производительности Поскольку копирование можно опустить, накладные расходы уменьшаются при обработке больших объемов данных.
  • Синхронизация потоков Вы можете использовать это вместе с Atomics для синхронизации и предотвращения конфликтов при доступе к памяти.

Пример основного использования

 1// Create a 16-byte shared memory
 2const sharedBuffer = new SharedArrayBuffer(16);
 3
 4// Treat it as an Int32Array
 5const sharedArray = new Int32Array(sharedBuffer);
 6
 7// Set a value
 8sharedArray[0] = 42;
 9
10console.log(sharedArray[0]);  // 42

В этом примере мы создаем область памяти размером 16 байт с помощью SharedArrayBuffer и рассматриваем эту память как Int32Array. Этот буфер памяти может быть использован несколькими потоками.

Использование с Web Workers

Истинная ценность SharedArrayBuffer проявляется при использовании вместе с Web Workers. Следующий код — пример использования общей памяти между главным потоком и рабочим потоком.

В главном потоке

 1// Create a shared buffer
 2const sharedBuffer = new SharedArrayBuffer(16);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create a worker
 6const worker = new Worker('worker.js');
 7
 8// Pass the shared buffer to the worker
 9worker.postMessage(sharedBuffer);
10
11// Modify the memory
12// Output : Main thread: 100
13sharedArray[0] = 100;
14console.log("Main thread: ", sharedArray[0]);

На стороне Worker (worker.js)

 1// worker.js
 2self.onmessage = function(event) {
 3    // Use the received shared buffer
 4    const sharedArray = new Int32Array(event.data);
 5
 6    // Read the contents of the memory
 7    // Output : Worker thread: 100
 8    console.log("Worker thread: ", sharedArray[0]);
 9
10    // Change the value
11    sharedArray[0] = 200;
12};
  • В этом примере главный поток создает общий буфер и передает его рабочему потоку. Рабочий поток может получить доступ к этому буферу для чтения и изменения значений. Таким образом, данные могут быть общими между потоками без копирования.

Двунаправленное подтверждение обновления

С помощью SharedArrayBuffer как главный поток, так и воркеры могут читать и записывать одну и ту же память, что позволяет подтверждать обновления в обоих направлениях. Ниже приведен пример, где главный поток устанавливает значение, воркер изменяет его, а затем главный поток проверяет обновление.

В главном потоке

 1// Create a shared buffer
 2const sharedBuffer = new SharedArrayBuffer(16);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create a worker
 6const worker = new Worker('worker.js');
 7
 8// Pass the shared buffer to the worker
 9worker.postMessage(sharedBuffer);
10
11// Set initial value
12// Output : Main thread initial: 100
13sharedArray[0] = 100;
14console.log("Main thread initial:", sharedArray[0]);
15
16// Listen for worker confirmation
17worker.onmessage = () => {
18    // Output : Main thread after worker update: 200
19    console.log("Main thread after worker update:", sharedArray[0]);
20};

На стороне Worker (worker.js)

 1// worker.js
 2self.onmessage = function(event) {
 3    const sharedArray = new Int32Array(event.data);
 4
 5    // Read initial value
 6    // Output : Worker thread received: 100
 7    console.log("Worker thread received:", sharedArray[0]);
 8
 9    // Update the value
10    sharedArray[0] = 200;
11
12    // Notify main thread
13    self.postMessage("Value updated");
14};
  • В этом примере главный поток сначала записывает значение 100, после чего воркер считывает его и изменяет на 200. После этого воркер уведомляет главный поток, и главный поток снова читает общую память для подтверждения обновления. Таким образом, комбинация уведомлений позволяет подтверждать обновления в обоих направлениях.

Синхронизация с Atomics

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

Например, чтобы безопасно увеличить значение счётчика с использованием нескольких потоков, вы можете воспользоваться Atomics для предотвращения конфликтов.

1const sharedBuffer = new SharedArrayBuffer(16);
2const sharedArray = new Int32Array(sharedBuffer);
3
4// Increment the counter
5Atomics.add(sharedArray, 0, 1);
6
7console.log(Atomics.load(sharedArray, 0));  // 1

Atomics.add атомарно увеличивает значение по указанному индексу и возвращает новое значение. Эта операция гарантированно выполняется без конфликтов с другими потоками. Atomics.load также используется для безопасного чтения значений из общей памяти.

Ожидание и уведомление с помощью Atomics.wait и Atomics.notify

При использовании SharedArrayBuffer бывают ситуации, когда воркеру необходимо ждать выполнения определенного условия, и как только другой воркер выполнит это условие, он должен уведомить ожидающего. В таких случаях полезны Atomics.wait и Atomics.notify.

Atomics.wait блокирует поток до тех пор, пока значение по конкретному индексу в общей памяти не изменится, а Atomics.notify уведомляет ожидающие потоки о возможности продолжения работы. Это обеспечивает безопасное ожидание и уведомления между несколькими воркерами. Однако Atomics.wait не может использоваться в основном потоке и доступен только внутри воркеров.

 1// Create a shared buffer (1 Int32 slot is enough for signaling)
 2const sharedBuffer = new SharedArrayBuffer(4);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create workers with names
 6const waiter = new Worker('worker.js', { name: 'waiter' });
 7const notifier = new Worker('worker.js', { name: 'notifier' });
 8
 9// Pass the shared buffer to both
10waiter.postMessage(sharedBuffer);
11notifier.postMessage(sharedBuffer);
12
13// Listen for messages
14waiter.onmessage = (event) => {
15    console.log(`[Main] Message from waiter:`, event.data);
16};
17notifier.onmessage = (event) => {
18    console.log(`[Main] Message from notifier:`, event.data);
19};
  • В основном потоке создаётся SharedArrayBuffer как разделяемая память и преобразуется в Int32Array с одним элементом. Этот единственный целочисленный слот используется в качестве сигнала для синхронизации между воркерами. Затем создаются два воркера, и каждому назначается роль с помощью свойства name: waiter (ожидающий) и notifier (уведомляющий). В конце общий буфер передаётся обоим воркерам, и настраиваются обработчики onmessage, чтобы можно было получать сообщения, отправленные каждым воркером.

На стороне Worker (worker.js)

 1// worker.js
 2onmessage = (event) => {
 3    const sharedArray = new Int32Array(event.data);
 4
 5    if (self.name === 'waiter') {
 6        postMessage('Waiter is waiting...');
 7        // Wait until notifier signals index 0
 8        Atomics.wait(sharedArray, 0, 0);
 9        postMessage('Waiter was notified!');
10    }
11
12    if (self.name === 'notifier') {
13        postMessage('Notifier is preparing...');
14        setTimeout(() => {
15            // Notify waiter after 2 seconds
16            Atomics.store(sharedArray, 0, 1);
17            Atomics.notify(sharedArray, 0, 1);
18            postMessage('Notifier has sent the signal!');
19        }, 2000);
20    }
21};
22// Output
23// [Main] Message from waiter: Waiter is waiting...
24// [Main] Message from notifier: Notifier is preparing...
25// [Main] Message from notifier: Notifier has sent the signal!
26// [Main] Message from waiter: Waiter was notified!
  • В этом примере воркер waiter остаётся в состоянии ожидания с помощью Atomics.wait, пока значение по индексу 0 равно 0. С другой стороны, когда воркер notifier изменяет значение на 123 с помощью Atomics.store и вызывает Atomics.notify, воркер waiter возобновляет выполнение и может получить обновлённое значение. Таким образом можно реализовать эффективное и безопасное ожидание и уведомление между потоками.

Сферы применения SharedArrayBuffer

SharedArrayBuffer особенно полезен в следующих случаях:.

  • Обработка в реальном времени Это подходит для приложений с низкой задержкой, таких как обработка аудио и видео или игровые движки, где данные должны мгновенно передаваться между потоками.
  • Параллельные вычисления При одновременной обработке больших объемов данных несколькими потоками использование SharedArrayBuffer позволяет избежать копирования памяти и повысить производительность.
  • Машинное обучение Параллелизация задач, таких как предварительная обработка данных и инференс, позволяет выполнять вычисления более эффективно.

Соображения безопасности

SharedArrayBuffer — это мощная функция, однако она также связана с рисками безопасности. В частности, опасения по поводу атак через побочные каналы, таких как Spectre, временно приостановили его поддержку. Для уменьшения этой уязвимости браузеры внедрили следующие меры:.

  • Изоляция сайтов Сайты, разрешающие использование SharedArrayBuffer, будут запущены в процессе, полностью изолированном от других сайтов.
  • Политика кросс-доменных ресурсов Для использования SharedArrayBuffer необходимо правильно настроить заголовки Cross-Origin-Opener-Policy и Cross-Origin-Embedder-Policy.

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

1Cross-Origin-Opener-Policy: same-origin
2Cross-Origin-Embedder-Policy: require-corp

Это предотвращает вмешательство внешних ресурсов в текущий контент и повышает безопасность.

Резюме

SharedArrayBuffer — это очень мощный инструмент для совместного использования памяти между несколькими потоками. Это важная технология для повышения производительности, и её эффективность особенно заметна в областях обработки в реальном времени и параллельных вычислений. Однако это также связано с рисками для безопасности, поэтому важны правильная конфигурация и синхронизация.

Используя SharedArrayBuffer, вы можете создавать более продвинутые и производительные веб-приложения.

Вы можете следовать этой статье, используя Visual Studio Code на нашем YouTube-канале. Пожалуйста, также посмотрите наш YouTube-канал.

YouTube Video