`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 stronieworker.tsten samSharedArrayBuffermoż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ękiAtomics.
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ękiAtomics.wait. W przeglądarkach nie można wywołaćAtomics.waitw głównym wątku. Aby zapobiec zamrażaniu interfejsu użytkownika,Atomics.waitmoż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
postMessageionmessagejest 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 ArrayBufferlubpostMessage, 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
Int32Arraynależ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
SharedArrayBufferjak zwykłyArrayBuffer, wystąpią konflikty danych. Zapisywanie pod ten sam indeks z wielu wątków bez użyciaAtomicsjest 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.