Lietuvių

Atraskite tikrą daugiagijį programavimą JavaScript. Šis išsamus vadovas apima SharedArrayBuffer, Atomics, Web Workers ir saugumo reikalavimus didelio našumo žiniatinklio programoms.

JavaScript SharedArrayBuffer: išsami pažintis su lygiagrečiu programavimu žiniatinklyje

Dešimtmečius JavaScript vienagijė prigimtis buvo ir jo paprastumo šaltinis, ir didelis našumo trūkumas. Įvykių ciklo modelis puikiai veikia daugumai su vartotojo sąsaja susijusių užduočių, tačiau susiduria su sunkumais, kai reikia atlikti skaičiavimams imlias operacijas. Ilgai trunkantys skaičiavimai gali užšaldyti naršyklę, sukeldami prastą vartotojo patirtį. Nors Web Workers pasiūlė dalinį sprendimą, leisdami scenarijams veikti fone, jie turėjo savo didelį apribojimą: neefektyvų duomenų perdavimą.

Čia pasirodo SharedArrayBuffer (SAB) – galinga funkcija, iš esmės keičianti žaidimo taisykles, įvesdama tikrą, žemo lygio atminties dalijimąsi tarp gijų žiniatinklyje. Kartu su Atomics objektu, SAB atveria naują didelio našumo, lygiagrečių programų erą tiesiog naršyklėje. Tačiau su didele galia ateina ir didelė atsakomybė – bei sudėtingumas.

Šis vadovas nuodugniai supažindins jus su lygiagretaus programavimo pasauliu JavaScript. Išnagrinėsime, kodėl mums to reikia, kaip veikia SharedArrayBuffer ir Atomics, kokius kritinius saugumo aspektus turite išspręsti, ir pateiksime praktinių pavyzdžių, padėsiančių jums pradėti.

Senasis pasaulis: JavaScript vienagijis modelis ir jo apribojimai

Prieš vertindami sprendimą, turime iki galo suprasti problemą. JavaScript vykdymas naršyklėje tradiciškai vyksta vienoje gijoje, dažnai vadinamoje „pagrindine gija“ arba „vartotojo sąsajos gija“.

Įvykių ciklas

Pagrindinė gija yra atsakinga už viską: jūsų JavaScript kodo vykdymą, puslapio atvaizdavimą, atsakymą į vartotojo sąveikas (pvz., paspaudimus ir slinkimą) ir CSS animacijų vykdymą. Ji valdo šias užduotis naudodama įvykių ciklą, kuris nuolat apdoroja pranešimų (užduočių) eilę. Jei užduotis užtrunka ilgai, ji blokuoja visą eilę. Nieko daugiau negali įvykti – vartotojo sąsaja užšąla, animacijos stringa, o puslapis tampa nereaguojantis.

Web Workers: žingsnis teisinga linkme

Web Workers buvo įdiegti siekiant sušvelninti šią problemą. Web Worker iš esmės yra scenarijus, veikiantis atskiroje foninėje gijoje. Galite perkelti sunkius skaičiavimus į „worker“, palikdami pagrindinę giją laisvą valdyti vartotojo sąsają.

Komunikacija tarp pagrindinės gijos ir „worker“ vyksta per postMessage() API. Kai siunčiate duomenis, jie apdorojami pagal struktūrinio klonavimo algoritmą. Tai reiškia, kad duomenys yra serializuojami, nukopijuojami ir tada deserializuojami „worker“ kontekste. Nors tai efektyvu, šis procesas turi didelių trūkumų dirbant su dideliais duomenų rinkiniais:

Įsivaizduokite vaizdo įrašų redaktorių naršyklėje. Siųsti visą vaizdo kadrą (kuris gali užimti kelis megabaitus) pirmyn ir atgal į „worker“ apdorojimui 60 kartų per sekundę būtų nepaprastai brangu. Būtent šią problemą ir buvo sukurtas išspręsti SharedArrayBuffer.

Situaciją keičiantis sprendimas: pristatome SharedArrayBuffer

SharedArrayBuffer yra fiksuoto ilgio neapdorotų dvejetainių duomenų buferis, panašus į ArrayBuffer. Esminis skirtumas yra tas, kad SharedArrayBuffer gali būti bendrinamas tarp kelių gijų (pvz., pagrindinės gijos ir vieno ar daugiau Web Workers). Kai „siunčiate“ SharedArrayBuffer naudodami postMessage(), jūs nesiunčiate kopijos; jūs siunčiate nuorodą į tą patį atminties bloką.

Tai reiškia, kad bet kokie vienos gijos atlikti pakeitimai buferio duomenyse yra akimirksniu matomi visoms kitoms gijoms, turinčioms nuorodą į jį. Tai pašalina brangų kopijavimo ir serializavimo žingsnį, įgalindama beveik momentinį duomenų dalijimąsi.

Pagalvokite apie tai šitaip:

Bendrinamos atminties pavojus: lenktynių sąlygos (Race Conditions)

Momentinis atminties dalijimasis yra galingas, tačiau jis taip pat sukelia klasikinę problemą iš lygiagretaus programavimo pasaulio: lenktynių sąlygas.

Lenktynių sąlygos atsiranda, kai kelios gijos bando vienu metu pasiekti ir modifikuoti tuos pačius bendrinamus duomenis, o galutinis rezultatas priklauso nuo nenuspėjamos jų vykdymo tvarkos. Apsvarstykite paprastą skaitiklį, saugomą SharedArrayBuffer. Tiek pagrindinė gija, tiek „worker“ nori jį padidinti.

  1. Gija A nuskaito esamą vertę, kuri yra 5.
  2. Prieš Gijai A įrašant naują vertę, operacinė sistema ją pristabdo ir persijungia į Giją B.
  3. Gija B nuskaito esamą vertę, kuri vis dar yra 5.
  4. Gija B apskaičiuoja naują vertę (6) ir įrašo ją atgal į atmintį.
  5. Sistema grįžta prie Gijos A. Ji nežino, kad Gija B ką nors padarė. Ji tęsia darbą nuo ten, kur baigė, apskaičiuodama savo naują vertę (5 + 1 = 6) ir įrašydama 6 atgal į atmintį.

Nors skaitiklis buvo padidintas du kartus, galutinė vertė yra 6, o ne 7. Operacijos nebuvo atominės – jos buvo pertraukiamos, o tai lėmė duomenų praradimą. Būtent todėl negalima naudoti SharedArrayBuffer be jo svarbiausio partnerio: Atomics objekto.

Bendrinamos atminties sargas: Atomics objektas

Atomics objektas suteikia statinių metodų rinkinį atominių operacijų atlikimui su SharedArrayBuffer objektais. Atominė operacija garantuotai atliekama visa apimtimi, nepertraukiama jokios kitos operacijos. Ji arba įvyksta visiškai, arba visai neįvyksta.

Naudojant Atomics išvengiama lenktynių sąlygų, užtikrinant, kad skaitymo-modifikavimo-rašymo operacijos su bendrinama atmintimi būtų atliekamos saugiai.

Svarbiausi Atomics metodai

Pažvelkime į keletą svarbiausių metodų, kuriuos teikia Atomics.

Sinchronizacija: daugiau nei paprastos operacijos

Kartais reikia daugiau nei tik saugaus skaitymo ir rašymo. Reikia, kad gijos koordinuotų savo veiksmus ir lauktų viena kitos. Dažnas anti-pavyzdys yra „aktyvus laukimas“ (busy-waiting), kai gija nuolatiniame cikle tikrina atminties vietą, ar joje neįvyko pakeitimų. Tai švaisto procesoriaus ciklus ir eikvoja baterijos energiją.

Atomics siūlo daug efektyvesnį sprendimą su wait() ir notify().

Viską sujungiame: praktinis vadovas

Dabar, kai suprantame teoriją, pereikime prie sprendimo, naudojant SharedArrayBuffer, įgyvendinimo žingsnių.

1 žingsnis: Saugumo prielaida - skirtingų šaltinių izoliacija

Tai yra dažniausia kliūtis programuotojams. Saugumo sumetimais, SharedArrayBuffer yra prieinamas tik puslapiuose, kurie yra skirtingų šaltinių izoliacijos būsenoje. Tai saugumo priemonė, skirta sušvelninti spekuliatyvaus vykdymo pažeidžiamumus, tokius kaip „Spectre“, kurie potencialiai galėtų naudoti aukštos raiškos laikmačius (įmanomus dėl bendrinamos atminties), kad nutekintų duomenis tarp skirtingų šaltinių.

Norėdami įjungti skirtingų šaltinių izoliaciją, turite sukonfigūruoti savo žiniatinklio serverį, kad jis siųstų dvi specifines HTTP antraštes jūsų pagrindiniam dokumentui:


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

Tai gali būti sudėtinga nustatyti, ypač jei naudojatės trečiųjų šalių scenarijais ar ištekliais, kurie neteikia reikiamų antraščių. Sukonfigūravę serverį, galite patikrinti, ar jūsų puslapis yra izoliuotas, naršyklės konsolėje patikrinę self.crossOriginIsolated savybę. Ji turi būti true.

2 žingsnis: Buferio sukūrimas ir bendrinimas

Savo pagrindiniame scenarijuje sukuriate SharedArrayBuffer ir jo „vaizdą“ (view) naudodami TypedArray, pavyzdžiui, Int32Array.

main.js:


// Pirmiausia patikrinkite, ar puslapis yra izoliuotas nuo kitų šaltinių!
if (!self.crossOriginIsolated) {
  console.error("Šis puslapis nėra izoliuotas nuo kitų šaltinių. SharedArrayBuffer nebus pasiekiamas.");
} else {
  // Sukurkite bendrinamą buferį vienam 32 bitų sveikajam skaičiui.
  const buffer = new SharedArrayBuffer(4);

  // Sukurkite buferio vaizdą. Visos atominės operacijos vykdomos su vaizdu.
  const int32Array = new Int32Array(buffer);

  // Inicializuokite vertę indekse 0.
  int32Array[0] = 0;

  // Sukurkite naują „worker“.
  const worker = new Worker('worker.js');

  // Nusiųskite BENDRINAMĄ buferį į „worker“. Tai nuorodos perdavimas, o ne kopija.
  worker.postMessage({ buffer });

  // Klausykitės pranešimų iš „worker“.
  worker.onmessage = (event) => {
    console.log(`„Worker“ pranešė apie baigimą. Galutinė vertė: ${Atomics.load(int32Array, 0)}`);
  };
}

3 žingsnis: Atominių operacijų atlikimas „worker“

„Worker“ gauna buferį ir dabar gali atlikti su juo atomines operacijas.

worker.js:


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

  console.log("„Worker“ gavo bendrinamą buferį.");

  // Atlikime keletą atominių operacijų.
  for (let i = 0; i < 1000000; i++) {
    // Saugiai padidinkime bendrinamą vertę.
    Atomics.add(int32Array, 0, 1);
  }

  console.log("„Worker“ baigė didinimą.");

  // Praneškite pagrindinei gijai, kad baigėme.
  self.postMessage({ done: true });
};

4 žingsnis: Sudėtingesnis pavyzdys - lygiagretus sumavimas su sinchronizacija

Išspręskime realistiškesnę problemą: susumuokime labai didelį skaičių masyvą naudodami kelis „worker“. Naudosime Atomics.wait() ir Atomics.notify() efektyviai sinchronizacijai.

Mūsų bendrinamas buferis turės tris dalis:

main.js:


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

  // [būsena, baigę_darbuotojai, rezultatas_žemas, rezultatas_aukštas]
  // Rezultatui naudojame du 32 bitų sveikuosius skaičius, kad išvengtume perpildymo didelėms sumoms.
  const sharedBuffer = new SharedArrayBuffer(4 * 4); // 4 sveikieji skaičiai
  const sharedArray = new Int32Array(sharedBuffer);

  // Sugeneruokime atsitiktinius duomenis apdorojimui
  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);
    
    // Sukurkite nebendrinamą vaizdą „worker“ duomenų daliai
    const dataChunk = data.subarray(start, end);

    worker.postMessage({ 
      sharedBuffer,
      dataChunk // Ši dalis yra kopijuojama
    });
  }

  console.log('Pagrindinė gija dabar laukia, kol „worker“ baigs darbą...');

  // Laukite, kol būsenos vėliavėlė indekse 0 taps 1
  // Tai daug geriau nei „while“ ciklas!
  Atomics.wait(sharedArray, 0, 0); // Laukite, jei sharedArray[0] yra 0

  console.log('Pagrindinė gija pažadinta!');
  const finalSum = Atomics.load(sharedArray, 2);
  console.log(`Galutinė lygiagreti suma yra: ${finalSum}`);

} else {
  console.error('Puslapis nėra izoliuotas nuo kitų šaltinių.');
}

sum_worker.js:


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

  // Apskaičiuokite sumą šio „worker“ duomenų daliai
  let localSum = 0;
  for (let i = 0; i < dataChunk.length; i++) {
    localSum += dataChunk[i];
  }

  // Atomiškai pridėkite vietinę sumą prie bendros sumos
  Atomics.add(sharedArray, 2, localSum);

  // Atomiškai padidinkite 'baigusių darbuotojų' skaitiklį
  const finishedCount = Atomics.add(sharedArray, 1, 1) + 1;

  // Jei tai paskutinis „worker“, kuris baigė...
  const NUM_WORKERS = 4; // Tikroje programoje turėtų būti perduodama
  if (finishedCount === NUM_WORKERS) {
    console.log('Paskutinis „worker“ baigė. Pranešama pagrindinei gijai.');

    // 1. Nustatykite būsenos vėliavėlę į 1 (baigta)
    Atomics.store(sharedArray, 0, 1);

    // 2. Praneškite pagrindinei gijai, kuri laukia ties indeksu 0
    Atomics.notify(sharedArray, 0, 1);
  }
};

Realūs panaudojimo atvejai ir programos

Kur ši galinga, bet sudėtinga technologija iš tikrųjų daro skirtumą? Ji puikiai tinka programoms, kurioms reikalingi sunkūs, paralelizmo reikalaujantys skaičiavimai su dideliais duomenų rinkiniais.

Iššūkiai ir galutiniai apmąstymai

Nors SharedArrayBuffer yra transformuojantis, tai nėra stebuklinga kulka. Tai žemo lygio įrankis, reikalaujantis atsargaus elgesio.

  1. Sudėtingumas: Lygiagretus programavimas yra žinomas kaip sudėtingas. Derinti lenktynių sąlygas ir aklavietes (deadlocks) gali būti neįtikėtinai sunku. Turite galvoti kitaip apie tai, kaip valdoma jūsų programos būsena.
  2. Aklavietės (Deadlocks): Aklavietė atsiranda, kai dvi ar daugiau gijų yra užblokuotos amžinai, kiekviena laukdama, kol kita atlaisvins resursą. Tai gali atsitikti, jei neteisingai įgyvendinsite sudėtingus užrakinimo mechanizmus.
  3. Saugumo pridėtinės išlaidos: Skirtingų šaltinių izoliacijos reikalavimas yra didelė kliūtis. Tai gali sutrikdyti integracijas su trečiųjų šalių paslaugomis, reklamomis ir mokėjimo sistemomis, jei jos nepalaiko būtinų CORS/CORP antraščių.
  4. Ne kiekvienai problemai: Paprastoms foninėms užduotims ar įvesties/išvesties operacijoms tradicinis Web Worker modelis su postMessage() dažnai yra paprastesnis ir pakankamas. SharedArrayBuffer naudokite tik tada, kai turite aiškų, procesoriui imlų našumo trūkumą, susijusį su dideliais duomenų kiekiais.

Išvada

SharedArrayBuffer, kartu su Atomics ir Web Workers, reiškia paradigmos poslinkį žiniatinklio programavime. Jis griauna vienagijio modelio ribas, kviesdamas į naršyklę naują galingų, našumo ir sudėtingumo programų klasę. Tai stato žiniatinklio platformą į lygesnę padėtį su vietinių programų kūrimu, kai kalbama apie skaičiavimams imlias užduotis.

Kelionė į lygiagretų JavaScript yra iššūkis, reikalaujantis griežto požiūrio į būsenos valdymą, sinchronizaciją ir saugumą. Tačiau programuotojams, siekiantiems peržengti žiniatinklyje įmanomų galimybių ribas – nuo realaus laiko garso sintezės iki sudėtingo 3D atvaizdavimo ir mokslinių skaičiavimų – SharedArrayBuffer įvaldymas nebėra tik pasirinkimas; tai esminis įgūdis kuriant naujos kartos žiniatinklio programas.