SharedArrayBuffer w JavaScript

SharedArrayBuffer w JavaScript

Ten artykuł wyjaśnia SharedArrayBuffer w JavaScript.

Przedstawimy szczegółowe wyjaśnienie podstaw SharedArrayBuffer, sposobów jego użycia, konkretnych zastosowań oraz kwestii bezpieczeństwa.

YouTube Video

SharedArrayBuffer w JavaScript

SharedArrayBuffer to potężne narzędzie w JavaScript do współdzielenia pamięci między wieloma wątkami. Szczególnie w połączeniu z Web Workerami umożliwia przetwarzanie równoległe, co czyni go efektywnym dla zadań obciążających obliczeniowo i aplikacji wymagających funkcji w czasie rzeczywistym.

Czym jest SharedArrayBuffer?

SharedArrayBuffer udostępnia bufor pamięci w JavaScript, który pozwala na współdzielenie danych binarnych między wieloma wątkami (głównie Web Workerami). Zwykły ArrayBuffer wymaga kopiowania między głównym wątkiem a workerami, ale SharedArrayBuffer umożliwia bezpośrednie współdzielenie pamięci bez kopiowania, co znacząco poprawia wydajność.

Funkcje

  • Pamięć współdzielona Pozwala wielu wątkom pracować w tej samej przestrzeni pamięci.
  • Poprawa wydajności Ponieważ kopiowanie może zostać pominięte, narzut zostaje zredukowany podczas przetwarzania dużych ilości danych.
  • Synchronizacja wątków Można jej używać razem z Atomics do synchronizacji w celu uniknięcia konfliktów podczas dostępu do pamięci.

Podstawowy Przykład Użytkowania

 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

W tym przykładzie tworzymy 16-bajtowy obszar pamięci za pomocą SharedArrayBuffer i traktujemy go jako Int32Array. Ten bufor pamięci może być współdzielony między wieloma wątkami.

Używanie z Web Workerami

Prawdziwa wartość SharedArrayBuffer ujawnia się, gdy jest używany w połączeniu z Web Workerami. Poniższy kod to przykład użycia współdzielonej pamięci między głównym wątkiem a workerem.

W głównym wątku

 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]);

Po stronie Workera (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};
  • W tym przykładzie główny wątek tworzy współdzielony bufor i przekazuje go workerowi. Worker może uzyskiwać dostęp do tego bufora, aby odczytywać i modyfikować wartości. W ten sposób dane mogą być współdzielone między wątkami bez konieczności kopiowania.

Dwukierunkowe potwierdzenie aktualizacji

Korzystając z SharedArrayBuffer, zarówno główny wątek, jak i workerzy mogą odczytywać i zapisywać te same dane w pamięci, umożliwiając dwukierunkowe potwierdzanie aktualizacji. Poniżej znajduje się przykład, w którym główny wątek ustawia wartość, worker ją zmienia, a następnie główny wątek sprawdza aktualizację.

W głównym wątku

 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};

Po stronie Workera (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};
  • W tym przykładzie główny wątek najpierw zapisuje wartość 100, po czym worker odczytuje ją i nadpisuje na 200. Następnie worker powiadamia główny wątek i główny wątek ponownie odczytuje współdzieloną pamięć, aby potwierdzić aktualizację. W ten sposób połączenie powiadomień umożliwia dwukierunkowe potwierdzanie aktualizacji.

Synchronizacja za pomocą Atomics

Podczas używania współdzielonej pamięci należy uważać na wyścigi danych i niespójności. Gdy wiele wątków jednocześnie uzyskuje dostęp do tej samej pamięci, mogą wystąpić konflikty. Aby temu zapobiec, JavaScript wykorzystuje obiekt Atomics do synchronizacji.

Na przykład, aby bezpiecznie zwiększyć licznik przy użyciu wielu wątków, można użyć Atomics, aby zapobiec konfliktom.

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 atomowo zwiększa wartość pod określonym indeksem i zwraca nową wartość. Ta operacja jest gwarantowana jako wolna od konfliktów z innymi wątkami. Atomics.load jest również używane do bezpiecznego odczytu wartości ze współdzielonej pamięci.

Oczekiwanie i powiadamianie z użyciem Atomics.wait i Atomics.notify

Podczas korzystania z SharedArrayBuffer zdarzają się sytuacje, gdy worker musi zaczekać na spełnienie określonego warunku, a gdy inny worker warunek ten spełni, musi powiadomić oczekującego workera. W takich przypadkach przydatne są Atomics.wait i Atomics.notify.

Atomics.wait blokuje wątek do czasu zmiany wartości pod określonym indeksem w pamięci współdzielonej, natomiast Atomics.notify powiadamia oczekujące wątki, że mogą wznowić pracę. Umożliwia to bezpieczne oczekiwanie i powiadamianie pomiędzy wieloma workerami. Jednak Atomics.wait nie może być używany w głównym wątku i jest dostępny tylko wewnątrz workerów.

 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};
  • W głównym wątku tworzony jest SharedArrayBuffer jako pamięć współdzielona i przekształcany w Int32Array z tylko jednym elementem. To pojedyncze miejsce na liczbę całkowitą jest używane jako sygnał do synchronizacji między workerami. Następnie tworzone są dwa workery, z których każdy otrzymuje rolę za pomocą właściwości name: waiter (rola oczekującego) i notifier (rola powiadamiającego). Na końcu wspólny bufor jest przekazywany do obu workerów, a obsługiwacze onmessage są ustawiane tak, aby można było odbierać wiadomości wysyłane z każdego workera.

Po stronie Workera (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!
  • W tym przykładzie worker waiter pozostaje w stanie oczekiwania przy użyciu Atomics.wait, dopóki wartość pod indeksem 0 wynosi 0. Z drugiej strony, gdy worker notifier zmienia wartość na 123 za pomocą Atomics.store i wywołuje Atomics.notify, worker waiter wznowi pracę i będzie mógł pobrać zaktualizowaną wartość. Dzięki temu można osiągnąć wydajne i bezpieczne oczekiwanie oraz powiadamianie między wątkami.

Przypadki użycia SharedArrayBuffer

SharedArrayBuffer jest szczególnie przydatny w następujących przypadkach użycia:.

  • Przetwarzanie w czasie rzeczywistym Nadaje się do aplikacji wymagających niskich opóźnień, takich jak przetwarzanie dźwięku i obrazu lub silniki gier, gdzie dane muszą być natychmiast współdzielone pomiędzy wątkami.
  • Obliczenia równoległe Podczas przetwarzania dużych ilości danych jednocześnie przez wiele wątków, zastosowanie SharedArrayBuffer pozwala uniknąć kopiowania pamięci i może poprawić wydajność.
  • Uczenie maszynowe Dzięki równoległemu wykonywaniu zadań, takich jak wstępne przetwarzanie danych i wnioskowanie, możliwe staje się efektywne obliczanie.

Zagadnienia bezpieczeństwa

SharedArrayBuffer to potężna funkcja, ale niesie ze sobą również ryzyko bezpieczeństwa. W szczególności obawy dotyczące ataków pośrednich, takich jak Spectre, tymczasowo wstrzymały jego obsługę. Aby złagodzić tę lukę, przeglądarki wdrożyły następujące środki:.

  • Izolacja stron Strony umożliwiające korzystanie z SharedArrayBuffer będą uruchamiane w procesie całkowicie odizolowanym od innych stron.
  • Polityka zasobów między źródłami (Cross-Origin Resource Policy) Aby korzystać z SharedArrayBuffer, nagłówki Cross-Origin-Opener-Policy oraz Cross-Origin-Embedder-Policy muszą być prawidłowo ustawione.

Na przykład, ustawiając nagłówki jak poniżej, możliwe staje się użycie SharedArrayBuffer:.

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

Zapobiega to ingerencji zasobów zewnętrznych w bieżącą zawartość i zwiększa bezpieczeństwo.

Podsumowanie

SharedArrayBuffer to bardzo potężne narzędzie do współdzielenia pamięci między wieloma wątkami. Jest to kluczowa technologia poprawiająca wydajność, a jej efekty są szczególnie widoczne w obszarze przetwarzania w czasie rzeczywistym i obliczeń równoległych. Jednak wiąże się to również z zagrożeniami dla bezpieczeństwa, dlatego ważne są prawidłowa konfiguracja i synchronizacja.

Wykorzystując SharedArrayBuffer, można budować bardziej zaawansowane i wydajne aplikacje internetowe.

Możesz śledzić ten artykuł, korzystając z Visual Studio Code na naszym kanale YouTube. Proszę również sprawdzić nasz kanał YouTube.

YouTube Video