Čeština

Odemkněte skutečný multithreading v JavaScriptu. Průvodce SharedArrayBuffer, Atomics a Web Workers pro vysoce výkonné webové aplikace.

JavaScript SharedArrayBuffer: Hloubkový pohled na souběžné programování na webu

Jednovláknová povaha JavaScriptu byla po desetiletí zdrojem jeho jednoduchosti, ale zároveň i významným výkonnostním omezením. Model smyčky událostí (event loop) funguje skvěle pro většinu úloh řízených uživatelským rozhraním, ale selhává při výpočetně náročných operacích. Dlouhotrvající výpočty mohou zamrazit prohlížeč a způsobit frustrující uživatelský zážitek. Ačkoli Web Workers nabídly částečné řešení tím, že umožnily spouštět skripty na pozadí, přinesly s sebou vlastní velké omezení: neefektivní datovou komunikaci.

Přichází SharedArrayBuffer (SAB), mocná funkce, která zásadně mění pravidla hry tím, že zavádí skutečné, nízkoúrovňové sdílení paměti mezi vlákny na webu. Ve spojení s objektem Atomics odemyká SAB novou éru vysoce výkonných, souběžných aplikací přímo v prohlížeči. S velkou mocí však přichází velká zodpovědnost – a složitost.

Tento průvodce vás zavede do hlubin světa souběžného programování v JavaScriptu. Prozkoumáme, proč ho potřebujeme, jak SharedArrayBuffer a Atomics fungují, klíčové bezpečnostní aspekty, kterým musíte věnovat pozornost, a praktické příklady, které vám pomohou začít.

Starý svět: Jednovláknový model JavaScriptu a jeho omezení

Než dokážeme ocenit řešení, musíme plně porozumět problému. Spouštění JavaScriptu v prohlížeči tradičně probíhá v jediném vlákně, často nazývaném „hlavní vlákno“ nebo „UI vlákno“.

Smyčka událostí (Event Loop)

Hlavní vlákno je zodpovědné za vše: spouštění vašeho JavaScriptového kódu, vykreslování stránky, reakce na interakce uživatele (jako jsou kliknutí a posouvání) a spouštění CSS animací. Tyto úkoly spravuje pomocí smyčky událostí, která neustále zpracovává frontu zpráv (úkolů). Pokud nějaký úkol trvá dlouho, zablokuje celou frontu. Nic jiného se nemůže dít – uživatelské rozhraní zamrzne, animace se zasekávají a stránka přestane reagovat.

Web Workers: Krok správným směrem

Web Workers byly zavedeny, aby tento problém zmírnily. Web Worker je v podstatě skript běžící v samostatném vlákně na pozadí. Můžete na něj přesunout náročné výpočty a udržet tak hlavní vlákno volné pro obsluhu uživatelského rozhraní.

Komunikace mezi hlavním vláknem a workerem probíhá prostřednictvím API postMessage(). Když posíláte data, jsou zpracována pomocí algoritmu strukturovaného klonování (structured clone algorithm). To znamená, že data jsou serializována, zkopírována a poté deserializována v kontextu workera. I když je tento proces efektivní, má pro velké datové sady významné nevýhody:

Představte si video editor v prohlížeči. Posílání celého video snímku (který může mít několik megabajtů) tam a zpět do workera ke zpracování 60krát za sekundu by bylo neúnosně nákladné. Přesně tento problém byl SharedArrayBuffer navržen k řešení.

Změna pravidel hry: Představení SharedArrayBuffer

SharedArrayBuffer je binární datový buffer s pevnou délkou, podobný ArrayBuffer. Klíčový rozdíl je v tom, že SharedArrayBuffer může být sdílen napříč několika vlákny (např. hlavním vláknem a jedním nebo více Web Workery). Když „posíláte“ SharedArrayBuffer pomocí postMessage(), neposíláte kopii; posíláte odkaz na tentýž blok paměti.

To znamená, že jakékoli změny provedené v datech bufferu jedním vláknem jsou okamžitě viditelné pro všechna ostatní vlákna, která na něj mají odkaz. Tím se eliminuje nákladný krok kopírování a serializace, což umožňuje téměř okamžité sdílení dat.

Představte si to takto:

Nebezpečí sdílené paměti: Souběhy (Race Conditions)

Okamžité sdílení paměti je mocné, ale také přináší klasický problém ze světa souběžného programování: souběhy (race conditions).

K souběhu dochází, když se více vláken pokouší přistupovat a upravovat stejná sdílená data současně a konečný výsledek závisí na nepředvídatelném pořadí, v jakém se provedou. Představte si jednoduchý čítač uložený v SharedArrayBuffer. Jak hlavní vlákno, tak worker ho chtějí inkrementovat.

  1. Vlákno A přečte aktuální hodnotu, která je 5.
  2. Než Vlákno A stihne zapsat novou hodnotu, operační systém ho pozastaví a přepne na Vlákno B.
  3. Vlákno B přečte aktuální hodnotu, která je stále 5.
  4. Vlákno B vypočítá novou hodnotu (6) a zapíše ji zpět do paměti.
  5. Systém se přepne zpět na Vlákno A. To neví, že Vlákno B něco udělalo. Pokračuje tam, kde skončilo, vypočítá svou novou hodnotu (5 + 1 = 6) a zapíše 6 zpět do paměti.

I když byl čítač inkrementován dvakrát, konečná hodnota je 6, ne 7. Operace nebyly atomické – byly přerušitelné, což vedlo ke ztrátě dat. Přesně proto nemůžete používat SharedArrayBuffer bez jeho klíčového partnera: objektu Atomics.

Strážce sdílené paměti: Objekt Atomics

Objekt Atomics poskytuje sadu statických metod pro provádění atomických operací na objektech SharedArrayBuffer. Atomická operace je zaručeně provedena vcelku, aniž by byla přerušena jakoukoli jinou operací. Buď se provede kompletně, nebo vůbec.

Použití Atomics zabraňuje souběhům tím, že zajišťuje bezpečné provedení operací typu čtení-úprava-zápis ve sdílené paměti.

Klíčové metody Atomics

Podívejme se na některé z nejdůležitějších metod, které Atomics poskytuje.

Synchronizace: Více než jen jednoduché operace

Někdy potřebujete víc než jen bezpečné čtení a zápis. Potřebujete, aby se vlákna koordinovala a čekala na sebe. Běžným anti-patternem je „aktivní čekání“ (busy-waiting), kdy vlákno sedí v těsné smyčce a neustále kontroluje paměťové místo, zda nedošlo ke změně. To plýtvá cykly CPU a vybíjí baterii.

Atomics poskytuje mnohem efektivnější řešení s metodami wait() a notify().

Vše dohromady: Praktický průvodce

Nyní, když rozumíme teorii, pojďme si projít kroky implementace řešení pomocí SharedArrayBuffer.

Krok 1: Bezpečnostní předpoklad – Cross-Origin Isolation

Toto je nejčastější překážka pro vývojáře. Z bezpečnostních důvodů je SharedArrayBuffer k dispozici pouze na stránkách, které jsou ve stavu cross-origin isolated. Jedná se o bezpečnostní opatření ke zmírnění zranitelností spekulativního spouštění, jako je Spectre, které by mohly potenciálně využít časovače s vysokým rozlišením (umožněné sdílenou pamětí) k úniku dat mezi různými původy (origins).

Pro povolení cross-origin izolace musíte nakonfigurovat váš webový server tak, aby pro váš hlavní dokument posílal dvě specifické HTTP hlavičky:


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

Nastavení může být náročné, zvláště pokud se spoléháte na skripty nebo zdroje třetích stran, které neposkytují potřebné hlavičky. Po nakonfigurování serveru můžete ověřit, zda je vaše stránka izolovaná, kontrolou vlastnosti self.crossOriginIsolated v konzoli prohlížeče. Musí být true.

Krok 2: Vytvoření a sdílení bufferu

Ve svém hlavním skriptu vytvoříte SharedArrayBuffer a „pohled“ na něj pomocí TypedArray, jako je Int32Array.

main.js:


// Nejprve zkontrolujte cross-origin izolaci!
if (!self.crossOriginIsolated) {
  console.error("Tato stránka není cross-origin isolated. SharedArrayBuffer nebude k dispozici.");
} else {
  // Vytvořte sdílený buffer pro jedno 32bitové celé číslo.
  const buffer = new SharedArrayBuffer(4);

  // Vytvořte pohled na buffer. Všechny atomické operace se provádějí na tomto pohledu.
  const int32Array = new Int32Array(buffer);

  // Inicializujte hodnotu na indexu 0.
  int32Array[0] = 0;

  // Vytvořte nového workera.
  const worker = new Worker('worker.js');

  // Odešlete SDÍLENÝ buffer workerovi. Jedná se o předání reference, nikoli o kopii.
  worker.postMessage({ buffer });

  // Naslouchejte zprávám od workera.
  worker.onmessage = (event) => {
    console.log(`Worker ohlásil dokončení. Konečná hodnota: ${Atomics.load(int32Array, 0)}`);
  };
}

Krok 3: Provádění atomických operací ve workeru

Worker obdrží buffer a nyní na něm může provádět atomické operace.

worker.js:


self.onmessage = (event) => {
  const { buffer } = event.data;
  const int32Array = new Int32Array(buffer);

  console.log("Worker obdržel sdílený buffer.");

  // Proveďme několik atomických operací.
  for (let i = 0; i < 1000000; i++) {
    // Bezpečně inkrementujte sdílenou hodnotu.
    Atomics.add(int32Array, 0, 1);
  }

  console.log("Worker dokončil inkrementaci.");

  // Dejte signál hlavnímu vláknu, že jsme hotovi.
  self.postMessage({ done: true });
};

Krok 4: Pokročilejší příklad – Paralelní sčítání se synchronizací

Pojďme se podívat na realističtější problém: sčítání velmi velkého pole čísel pomocí více workerů. Pro efektivní synchronizaci použijeme Atomics.wait() a Atomics.notify().

Náš sdílený buffer bude mít tři části:

main.js:


if (self.crossOriginIsolated) {
  const NUM_WORKERS = 4;
  const DATA_SIZE = 10_000_000;

  // [stav, workeri_dokonceno, vysledek_nizsi, vysledek_vyssi]
  // Používáme dvě 32bitová celá čísla pro výsledek, abychom se vyhnuli přetečení u velkých součtů.
  const sharedBuffer = new SharedArrayBuffer(4 * 4); // 4 celá čísla
  const sharedArray = new Int32Array(sharedBuffer);

  // Vygenerujte náhodná data ke zpracování
  const data = new Uint8Array(DATA_SIZE);
  for (let i = 0; i < DATA_SIZE; i++) {
    data[i] = Math.floor(Math.random() * 10);
  }

  const chunkSize = Math.ceil(DATA_SIZE / NUM_WORKERS);

  for (let i = 0; i < NUM_WORKERS; i++) {
    const worker = new Worker('sum_worker.js');
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, DATA_SIZE);
    
    // Vytvořte nesdílený pohled pro část dat daného workera
    const dataChunk = data.subarray(start, end);

    worker.postMessage({ 
      sharedBuffer,
      dataChunk // Toto se kopíruje
    });
  }

  console.log('Hlavní vlákno nyní čeká na dokončení workerů...');

  // Počkejte, dokud se příznak stavu na indexu 0 nezmění na 1
  // Je to mnohem lepší než while smyčka!
  Atomics.wait(sharedArray, 0, 0); // Čekej, pokud je sharedArray[0] rovno 0

  console.log('Hlavní vlákno bylo probuzeno!');
  const finalSum = Atomics.load(sharedArray, 2);
  console.log(`Konečný paralelní součet je: ${finalSum}`);

} else {
  console.error('Stránka není cross-origin isolated.');
}

sum_worker.js:


self.onmessage = ({ data }) => {
  const { sharedBuffer, dataChunk } = data;
  const sharedArray = new Int32Array(sharedBuffer);

  // Vypočítejte součet pro část dat tohoto workera
  let localSum = 0;
  for (let i = 0; i < dataChunk.length; i++) {
    localSum += dataChunk[i];
  }

  // Atomicky přičtěte lokální součet ke sdílenému celkovému součtu
  Atomics.add(sharedArray, 2, localSum);

  // Atomicky inkrementujte čítač 'dokončených workerů'
  const finishedCount = Atomics.add(sharedArray, 1, 1) + 1;

  // Pokud je toto poslední worker, který dokončil práci...
  const NUM_WORKERS = 4; // V reálné aplikaci by se mělo předávat jako parametr
  if (finishedCount === NUM_WORKERS) {
    console.log('Poslední worker dokončil. Upozorňuji hlavní vlákno.');

    // 1. Nastavte příznak stavu na 1 (dokončeno)
    Atomics.store(sharedArray, 0, 1);

    // 2. Upozorněte hlavní vlákno, které čeká na indexu 0
    Atomics.notify(sharedArray, 0, 1);
  }
};

Případy použití a aplikace v reálném světě

Kde tato mocná, ale složitá technologie skutečně přináší změnu? Vyniká v aplikacích, které vyžadují náročné, paralelizovatelné výpočty nad velkými datovými sadami.

Výzvy a závěrečná zvážení

Ačkoli je SharedArrayBuffer transformační, není to všelék. Je to nízkoúrovňový nástroj, který vyžaduje opatrné zacházení.

  1. Složitost: Souběžné programování je notoricky obtížné. Ladění souběhů a deadlocků může být neuvěřitelně náročné. Musíte přemýšlet jinak o tom, jak je spravován stav vaší aplikace.
  2. Deadlocky (zablokování): K deadlocku dochází, když jsou dvě nebo více vláken navždy zablokována, přičemž každé čeká, až to druhé uvolní zdroj. To se může stát, pokud nesprávně implementujete složité zamykací mechanismy.
  3. Bezpečnostní režie: Požadavek na cross-origin izolaci je významnou překážkou. Může narušit integrace se službami třetích stran, reklamami a platebními bránami, pokud nepodporují potřebné hlavičky CORS/CORP.
  4. Není pro každý problém: Pro jednoduché úlohy na pozadí nebo I/O operace je tradiční model Web Worker s postMessage() často jednodušší a dostačující. Po SharedArrayBuffer sáhněte pouze tehdy, když máte jasné, CPU-vázané úzké hrdlo zahrnující velké množství dat.

Závěr

SharedArrayBuffer, ve spojení s Atomics a Web Workers, představuje změnu paradigmatu pro webový vývoj. Boří hranice jednovláknového modelu a zve do prohlížeče novou třídu výkonných, efektivních a komplexních aplikací. Staví webovou platformu na roveň s nativním vývojem aplikací pro výpočetně náročné úkoly.

Cesta do souběžného JavaScriptu je náročná a vyžaduje pečlivý přístup ke správě stavu, synchronizaci a bezpečnosti. Ale pro vývojáře, kteří chtějí posouvat hranice toho, co je na webu možné – od syntézy zvuku v reálném čase po komplexní 3D vykreslování a vědecké výpočty – zvládnutí SharedArrayBuffer již není jen možností; je to nezbytná dovednost pro budování webových aplikací nové generace.

JavaScript SharedArrayBuffer: Hloubkový pohled na souběžné programování na webu | MLOG