`SharedArrayBuffer` w TypeScript

`SharedArrayBuffer` w TypeScript

Ten artykuł wyjaśnia SharedArrayBuffer w TypeScript.

Wyjaśnimy SharedArrayBuffer w TypeScript na praktycznych przykładach.

YouTube Video

SharedArrayBuffer w TypeScript

SharedArrayBuffer to mechanizm umożliwiający współdzielenie tej samej przestrzeni pamięci pomiędzy wieloma wątkami, takimi jak Web Workers. Łącząc to z Atomics, można zarządzać konfliktami danych i wykonywać operacje na współdzielonej pamięci o niskich opóźnieniach.

Wymagania wstępne i uwagi

Korzystając z SharedArrayBuffer w przeglądarce, wymagane są nagłówki COOP i COEP spełniające wymogi bezpieczeństwa znane jako cross-origin isolation (izolacja między źródłami). W Node.js pamięcią współdzieloną można stosunkowo łatwo zarządzać przy użyciu worker_threads.

Podstawowe pojęcia

SharedArrayBuffer to obiekt reprezentujący sekwencję bajtów o stałej długości, a liczby można czytać i zapisywać przez TypedArray i podobne widoki. Proste operacje odczytu/zapisu nie są synchronizowane, dlatego do zapewnienia atomowości operacji używa się API Atomics oraz mechanizmów wait i notify do koordynacji.

Prosty licznik (wersja przeglądarkowa)

W tym przykładzie główny wątek tworzy SharedArrayBuffer i przekazuje go do Web Worker, a oba wątki zwiększają wspólny licznik. To pokazuje minimalny wzorzec: dodawanie atomowe za pomocą Atomics.add i odczyt przez Atomics.load.

main.ts (strona przeglądarkowa)

Przykład ten pokazuje, jak główny wątek tworzy SharedArrayBuffer i współdzieli go z Workerem w celu uzyskania dostępu wielowątkowego.

 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};
  • W tym kodzie główny wątek używa Atomics.add, aby atomowo zwiększyć wartość. Po stronie worker.ts ten sam SharedArrayBuffer może być odczytywany i modyfikowany.

worker.ts (Worker w przeglądarce)

Oto przykład, w którym worker otrzymuje ten sam współdzielony bufor i okresowo go zmniejsza lub w inny sposób manipuluje.

 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 również manipuluje tą samą pamięcią poprzez Int32Array, a aktualizacje są wykonywane bez konfliktów danych dzięki Atomics.

Synchronizacja za pomocą wait/notify

Stosując Atomics.wait i Atomics.notify, można wstrzymywać wątki do momentu spełnienia określonych warunków, umożliwiając synchronizację zdarzeniową pomiędzy workerami. W przeglądarkach najbezpieczniej jest używać Atomics.wait wewnątrz Workera.

producer.ts (Worker produkujący w przeglądarce)

Producent zapisuje dane i powiadamia konsumenta przy użyciu 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 konsumujący w przeglądarce)

Konsument czeka przy użyciu Atomics.wait i wznawia przetwarzanie po powiadomieniu przez producenta.

 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};
  • W tym wzorcu producent powiadamia konsumenta za pomocą Atomics.notify, a konsument efektywnie oczekuje dzięki Atomics.wait. W przeglądarkach nie można wywołać Atomics.wait w głównym wątku. Aby zapobiec zamrażaniu interfejsu użytkownika, Atomics.wait można używać tylko wewnątrz Workerów.

Praktyczny przykład z Node.js (worker_threads)

W środowisku Node.js funkcjonalność SharedArrayBuffer można obsłużyć przy użyciu worker_threads. Poniżej znajduje się przykładowy kod z typami w TypeScript dla Node.js.

main-node.ts

Główny wątek tworzy bufor i przekazuje go do Workera.

 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

Ten przykład wykorzystuje parentPort i workerData z worker_threads po stronie Workera.

 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);
  • W Node.js nie ma takich ograniczeń COOP ani COEP jak w przeglądarkach – współdzieloną pamięć można łatwo obsługiwać po prostu używając worker_threads. Korzystając z TypeScript, zwróć uwagę, czy budujesz projekt z ustawieniami CommonJS czy ESM.

Wskazówki odnośnie typowania w TypeScript

SharedArrayBuffer oraz Atomics są zawarte w standardowych definicjach typów DOM i bibliotek, dzięki czemu można je bezpośrednio używać w TypeScript. Przesyłając wiadomości między Workerami, bezpieczniej jest definiować interfejsy i jednoznacznie określać typy.

1// Example: typed message
2type WorkerMessage = { type: 'init'; sab: SharedArrayBuffer } | { type: 'ping' };
  • Jawne określenie typów sprawia, że obsługa postMessage i onmessage jest bezpieczniejsza i umożliwia sprawdzanie typów.

Praktyczne zastosowania

SharedArrayBuffer nie zawsze jest konieczny, ale jest bardzo skuteczny w sytuacjach, gdy wymagane są korzyści szybkościowe wynikające ze współdzielonej pamięci. Zrozumienie sytuacji, w których jest skuteczny, prowadzi do odpowiedniego wyboru technologii.

  • Nadaje się do przetwarzania o niskich opóźnieniach wymagających szybkich współdzielonych buforów, na przykład do przetwarzania audio/wideo w czasie rzeczywistym lub fizyki w grach.
  • Do prostych wymian lub przesyłania dużej ilości danych łatwiej może być użyć Transferable ArrayBuffer lub postMessage, niż SharedArrayBuffer.

Ograniczenia i bezpieczeństwo

Aby użyć SharedArrayBuffer w przeglądarce, wymagana jest izolacja między źródłami: należy ustawić COOP na same-origin-allow-popups, a COEP na require-corp. SharedArrayBuffer zostanie wyłączony, jeśli te wymagania nie zostaną spełnione.

Wskazówki dotyczące wydajności i debugowania

Operacje atomowe (Atomics) są szybkie, ale częste oczekiwanie i nadmierna synchronizacja mogą zwiększyć opóźnienia.

Aby bezpiecznie i wydajnie obsługiwać pamięć współdzieloną, należy sprawdzić poniższe punkty.

  • Widoki takie jak Int32Array należy obsługiwać z zachowaniem odpowiedniego wyrównania bajtów.
  • Miej jasność, które indeksy tego samego bufora są używane przez poszczególne procesy i zachowuj spójne konwencje w kodzie.
  • Traktując SharedArrayBuffer jak zwykły ArrayBuffer, wystąpią konflikty danych. Zapisywanie pod ten sam indeks z wielu wątków bez użycia Atomics jest niebezpieczne.

Podsumowanie

Dzięki właściwemu użyciu SharedArrayBuffer i Atomics można uzyskać bezpieczne i szybkie operacje na pamięci współdzielonej nawet w TypeScript. Zaczynając od prostych wzorców synchronizacji, takich jak liczniki lub sygnały, łatwiej zrozumieć zasadę działania, a dzięki właściwemu zarządzaniu synchronizacją i indeksami można efektywnie działać także w scenariuszach wymagających niskich opóźnień.

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

YouTube Video