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:
- Našumo pridėtinės išlaidos: Megabaitų ar net gigabaitų duomenų kopijavimas tarp gijų yra lėtas ir reikalauja daug procesoriaus resursų.
- Atminties suvartojimas: Sukuriama duomenų kopija atmintyje, o tai gali būti didelė problema įrenginiams su ribota atmintimi.
Į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:
- Web Workers su
postMessage()
: Tai panašu į du kolegas, dirbančius su dokumentu, siunčiant kopijas el. paštu. Kiekvienam pakeitimui reikia siųsti visiškai naują kopiją. - Web Workers su
SharedArrayBuffer
: Tai panašu į du kolegas, dirbančius su tuo pačiu dokumentu bendrinamame internetiniame redaktoriuje (pvz., „Google Docs“). Pakeitimai abiem matomi realiu laiku.
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.
- Gija A nuskaito esamą vertę, kuri yra 5.
- Prieš Gijai A įrašant naują vertę, operacinė sistema ją pristabdo ir persijungia į Giją B.
- Gija B nuskaito esamą vertę, kuri vis dar yra 5.
- Gija B apskaičiuoja naują vertę (6) ir įrašo ją atgal į atmintį.
- 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
.
Atomics.load(typedArray, index)
: Atomiškai nuskaito vertę nurodytame indekse ir ją grąžina. Tai užtikrina, kad skaitote pilną, nepažeistą vertę.Atomics.store(typedArray, index, value)
: Atomiškai išsaugo vertę nurodytame indekse ir grąžina tą vertę. Tai užtikrina, kad rašymo operacija nebus pertraukta.Atomics.add(typedArray, index, value)
: Atomiškai prideda vertę prie vertės, esančios nurodytame indekse. Grąžina originalią vertę toje pozicijoje. Tai atominisx += value
atitikmuo.Atomics.sub(typedArray, index, value)
: Atomiškai atima vertę iš vertės, esančios nurodytame indekse.Atomics.compareExchange(typedArray, index, expectedValue, replacementValue)
: Tai galingas sąlyginis rašymas. Jis patikrina, ar vertė tiesindex
yra lygiexpectedValue
. Jei taip, jis pakeičia jąreplacementValue
ir grąžina originaliąexpectedValue
. Jei ne, jis nieko nedaro ir grąžina esamą vertę. Tai yra fundamentalus statybinis blokas sudėtingesniems sinchronizacijos primityvams, pvz., užraktams, įgyvendinti.
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()
.
Atomics.wait(typedArray, index, value, timeout)
: Šis metodas liepia gijai „užmigti“. Jis patikrina, ar vertė tiesindex
vis dar yravalue
. Jei taip, gija miega, kol ją pažadinsAtomics.notify()
arba kol pasibaigs neprivalomastimeout
(milisekundėmis). Jei vertė tiesindex
jau pasikeitė, metodas grįžta nedelsiant. Tai neįtikėtinai efektyvu, nes mieganti gija beveik nenaudoja procesoriaus resursų.Atomics.notify(typedArray, index, count)
: Naudojamas pažadinti gijas, kurios miega tam tikroje atminties vietoje naudodamosAtomics.wait()
. Jis pažadins ne daugiau kaipcount
laukiančių gijų (arba visas, jeicount
nenurodytas arba yraInfinity
).
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
Cross-Origin-Opener-Policy: same-origin
(COOP): Izoliuoja jūsų dokumento naršymo kontekstą nuo kitų dokumentų, neleidžiant jiems tiesiogiai sąveikauti su jūsų lango objektu.Cross-Origin-Embedder-Policy: require-corp
(COEP): Reikalauja, kad visi antriniai ištekliai (pvz., paveikslėliai, scenarijai ir iframe), kuriuos įkelia jūsų puslapis, būtų iš to paties šaltinio arba aiškiai pažymėti kaip įkeliami iš kito šaltinio suCross-Origin-Resource-Policy
antrašte arba CORS.
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:
- Indeksas 0: Būsenos vėliavėlė (0 = apdorojama, 1 = baigta).
- Indeksas 1: Skaitiklis, rodantis, kiek „worker“ baigė darbą.
- Indeksas 2: Galutinė suma.
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.
- WebAssembly (Wasm): Tai yra pagrindinis panaudojimo atvejis. Kalbos kaip C++, Rust ir Go turi brandų daugiagijo programavimo palaikymą. Wasm leidžia programuotojams kompiliuoti šias esamas didelio našumo, daugiagijes programas (pvz., žaidimų variklius, CAD programinę įrangą ir mokslinius modelius), kad jos veiktų naršyklėje, naudojant
SharedArrayBuffer
kaip pagrindinį mechanizmą gijų komunikacijai. - Duomenų apdorojimas naršyklėje: Didelio masto duomenų vizualizacija, kliento pusės mašininio mokymosi modelių išvedimas ir mokslinės simuliacijos, apdorojančios didžiulius duomenų kiekius, gali būti žymiai pagreitintos.
- Medijos redagavimas: Filtrų taikymas didelės raiškos vaizdams arba garso apdorojimas garso faile gali būti suskaidytas į dalis ir apdorotas lygiagrečiai kelių „worker“, suteikiant vartotojui grįžtamąjį ryšį realiu laiku.
- Aukšto našumo žaidimai: Modernūs žaidimų varikliai labai priklauso nuo daugiagijo programavimo fizikai, dirbtiniam intelektui ir turinio įkėlimui.
SharedArrayBuffer
leidžia kurti konsolės kokybės žaidimus, kurie veikia visiškai naršyklėje.
Iššūkiai ir galutiniai apmąstymai
Nors SharedArrayBuffer
yra transformuojantis, tai nėra stebuklinga kulka. Tai žemo lygio įrankis, reikalaujantis atsargaus elgesio.
- 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.
- 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.
- 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ų.
- 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.