Avage JavaScriptis tõeline mitmelõimelisus. See põhjalik juhend käsitleb SharedArrayBufferit, Atomicsit, Web Workereid ja turvanõudeid suure jõudlusega veebirakenduste jaoks.
JavaScript SharedArrayBuffer: Süvauuring Konkurentsest Programmeerimisest Veebis
Aastakümneid on JavaScripti ühelõimelisus olnud nii selle lihtsuse allikas kui ka oluline jõudluse kitsaskoht. Sündmuste tsükli mudel töötab suurepäraselt enamiku kasutajaliidese-põhiste ülesannete jaoks, kuid satub raskustesse arvutusmahukate operatsioonide puhul. Pikaajalised arvutused võivad brauseri külmutada, luues frustreeriva kasutajakogemuse. Kuigi Web Workerid pakkusid osalise lahenduse, võimaldades skriptidel taustal joosta, oli neil oma suur piirang: ebaefektiivne andmeside.
Siin tuleb mängu SharedArrayBuffer
(SAB), võimas funktsioon, mis muudab mängu põhjalikult, tuues veebi tõelise, madala taseme mälu jagamise lõimede vahel. Koos Atomics
-objektiga avab SAB uue ajastu suure jõudlusega, konkurentsete rakenduste jaoks otse brauseris. Kuid suure võimuga kaasneb suur vastutus – ja keerukus.
See juhend viib teid süvitsi JavaScripti konkurentse programmeerimise maailma. Uurime, miks me seda vajame, kuidas SharedArrayBuffer
ja Atomics
töötavad, milliseid kriitilisi turvalisuskaalutlusi peate arvestama, ja praktilisi näiteid alustamiseks.
Vana maailm: JavaScripti ühelõimeline mudel ja selle piirangud
Enne kui saame lahendust hinnata, peame probleemi täielikult mõistma. JavaScripti täitmine brauseris toimub traditsiooniliselt ühel lõimel, mida sageli nimetatakse "põhilõimeks" või "kasutajaliidese lõimeks".
Sündmuste tsükkel
Põhilõim vastutab kõige eest: teie JavaScripti koodi täitmise, lehe renderdamise, kasutaja interaktsioonidele (nagu klõpsud ja kerimised) reageerimise ja CSS-animatsioonide käitamise eest. See haldab neid ülesandeid sündmuste tsükli abil, mis töötleb pidevalt sõnumite (ülesannete) järjekorda. Kui ülesande täitmine võtab kaua aega, blokeerib see kogu järjekorra. Midagi muud ei saa juhtuda – kasutajaliides hangub, animatsioonid hakivad ja leht muutub mittereageerivaks.
Web Workerid: Samm õiges suunas
Web Workerid loodi selle probleemi leevendamiseks. Web Worker on sisuliselt skript, mis töötab eraldi taustalõimel. Saate rasked arvutused delegeerida workerile, hoides põhilõime vaba kasutajaliidese haldamiseks.
Suhtlus põhilõime ja workeri vahel toimub postMessage()
API kaudu. Andmete saatmisel kasutatakse struktureeritud kloonimise algoritmi. See tähendab, et andmed serialiseeritakse, kopeeritakse ja seejärel deserialiseeritakse workeri kontekstis. Kuigi see on tõhus, on sellel protsessil suurte andmekogumite puhul olulisi puudusi:
- Jõudluse lisakulu: Megabaitide või isegi gigabaitide andmete kopeerimine lõimede vahel on aeglane ja protsessorimahukas.
- Mälukasutus: See loob mällu andmetest duplikaadi, mis võib olla suur probleem piiratud mäluga seadmete jaoks.
Kujutage ette videoredaktorit brauseris. Terve videokaadri (mis võib olla mitu megabaiti) edasi-tagasi saatmine workerile töötlemiseks 60 korda sekundis oleks ülemäära kulukas. See on täpselt see probleem, mille lahendamiseks SharedArrayBuffer
loodi.
Mängumuutja: Tutvustame SharedArrayBuffer
'it
SharedArrayBuffer
on fikseeritud pikkusega toores binaarne andmepuhver, sarnane ArrayBuffer
'ile. Kriitiline erinevus on see, et SharedArrayBuffer
'it saab jagada mitme lõime vahel (nt põhilõim ja üks või mitu Web Workerit). Kui te "saadate" SharedArrayBuffer
'i kasutades postMessage()
, ei saada te koopiat; te saadate viite samale mälublokile.
See tähendab, et kõik muudatused, mida üks lõim puhvri andmetes teeb, on koheselt nähtavad kõigile teistele lõimedele, millel on sellele viide. See kõrvaldab kuluka kopeerimise ja serialiseerimise sammu, võimaldades peaaegu hetkelist andmete jagamist.
Mõelge sellest nii:
- Web Workerid
postMessage()
'ga: See on nagu kaks kolleegi, kes töötavad dokumendi kallal, saates e-kirjaga koopiaid edasi-tagasi. Iga muudatus nõuab terve uue koopia saatmist. - Web Workerid
SharedArrayBuffer
'iga: See on nagu kaks kolleegi, kes töötavad sama dokumendi kallal jagatud veebiredaktoris (nagu Google Docs). Muudatused on mõlemale reaalajas nähtavad.
Jagatava mälu oht: Võidujooksu tingimused
Kohene mälu jagamine on võimas, kuid see toob kaasa ka klassikalise probleemi konkurentse programmeerimise maailmast: võidujooksu tingimused.
Võidujooksu tingimus tekib siis, kui mitu lõime üritavad samaaegselt ligi pääseda ja muuta samu jagatud andmeid ning lõpptulemus sõltub nende ettearvamatust täitmise järjekorrast. Kujutage ette lihtsat loendurit, mis on salvestatud SharedArrayBuffer
'isse. Nii põhilõim kui ka worker tahavad seda suurendada.
- Lõim A loeb hetkeväärtuse, mis on 5.
- Enne kui Lõim A saab uue väärtuse kirjutada, peatab operatsioonisüsteem selle ja lülitub Lõimele B.
- Lõim B loeb hetkeväärtuse, mis on endiselt 5.
- Lõim B arvutab uue väärtuse (6) ja kirjutab selle tagasi mällu.
- Süsteem lülitub tagasi Lõimele A. See ei tea, et Lõim B midagi tegi. See jätkab sealt, kus pooleli jäi, arvutades oma uue väärtuse (5 + 1 = 6) ja kirjutades 6 tagasi mällu.
Kuigi loendurit suurendati kaks korda, on lõppväärtus 6, mitte 7. Operatsioonid ei olnud atomaarsed – need olid katkestatavad, mis viis andmete kadumiseni. Just seetõttu ei saa te kasutada SharedArrayBuffer
'it ilma selle olulise partnerita: Atomics
-objektita.
Jagatava mälu valvur: Atomics
-objekt
Atomics
-objekt pakub staatiliste meetodite komplekti atomaarsete operatsioonide teostamiseks SharedArrayBuffer
-objektidel. Atomaarne operatsioon on garanteeritult täielikult teostatud, ilma et seda katkestaks ükski teine operatsioon. See kas juhtub täielikult või üldse mitte.
Atomics
'i kasutamine hoiab ära võidujooksu tingimused, tagades, et loe-muuda-kirjuta operatsioonid jagatud mäluga teostatakse ohutult.
Põhilised Atomics
-meetodid
Vaatame mõningaid kõige olulisemaid meetodeid, mida Atomics
pakub.
Atomics.load(typedArray, index)
: Loeb atomaarselt väärtuse antud indeksilt ja tagastab selle. See tagab, et loete täielikku, rikkumata väärtust.Atomics.store(typedArray, index, value)
: Salvestab atomaarselt väärtuse antud indeksile ja tagastab selle väärtuse. See tagab, et kirjutamisoperatsiooni ei katkestata.Atomics.add(typedArray, index, value)
: Liidab atomaarselt väärtuse antud indeksi väärtusele. See tagastab selle positsiooni algse väärtuse. See on atomaarne ekvivalentx += value
'le.Atomics.sub(typedArray, index, value)
: Lahutab atomaarselt väärtuse antud indeksi väärtusest.Atomics.compareExchange(typedArray, index, expectedValue, replacementValue)
: See on võimas tingimuslik kirjutamine. See kontrollib, kas väärtusindex
'il on võrdneexpectedValue
'ga. Kui on, asendab see sellereplacementValue
'ga ja tagastab algseexpectedValue
. Kui mitte, ei tee see midagi ja tagastab hetkeväärtuse. See on fundamentaalne ehitusplokk keerukamate sünkroniseerimisprimitiivide, näiteks lukkude, implementeerimiseks.
Sünkroniseerimine: Rohkem kui lihtsad operatsioonid
Mõnikord on vaja enamat kui lihtsalt ohutut lugemist ja kirjutamist. On vaja, et lõimed koordineeriksid ja ootaksid üksteist. Levinud antipattern on "hõivatud ootamine", kus lõim istub tihedas tsüklis, kontrollides pidevalt mälukohta muutuse suhtes. See raiskab protsessori tsükleid ja tühjendab akut.
Atomics
pakub palju tõhusama lahenduse wait()
ja notify()
abil.
Atomics.wait(typedArray, index, value, timeout)
: See käsib lõimel magama minna. See kontrollib, kas väärtusindex
'il on endiseltvalue
. Kui jah, siis lõim magab, kuni see äratatakseAtomics.notify()
abil või kuni valikulinetimeout
(millisekundites) on saavutatud. Kui väärtusindex
'il on juba muutunud, tagastab see kohe. See on uskumatult tõhus, kuna magav lõim tarbib peaaegu üldse protsessori ressursse.Atomics.notify(typedArray, index, count)
: Seda kasutatakse lõimede äratamiseks, mis magavad kindlas mälukohasAtomics.wait()
kaudu. See äratab maksimaalseltcount
ootavat lõime (või kõik, kuicount
'i pole antud või onInfinity
).
Kõike kokku pannes: Praktiline juhend
Nüüd, kui me mõistame teooriat, vaatame läbi sammud lahenduse implementeerimiseks, kasutades SharedArrayBuffer
'it.
1. samm: Turvalisuse eeltingimus – päritoluülene isoleerimine
See on arendajate jaoks kõige levinum komistuskivi. Turvalisuse kaalutlustel on SharedArrayBuffer
saadaval ainult lehtedel, mis on päritoluüleselt isoleeritud olekus. See on turvameede spekulatiivse täitmise haavatavuste, nagu Spectre, leevendamiseks, mis võiksid potentsiaalselt kasutada kõrge resolutsiooniga taimereid (mida jagatud mälu võimaldab) andmete lekitamiseks päritolude vahel.
Päritoluülese isoleerimise lubamiseks peate konfigureerima oma veebiserveri saatma oma põhidokumendi jaoks kaks spetsiifilist HTTP-päist:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
(COOP): Isoleerib teie dokumendi sirvimiskonteksti teistest dokumentidest, takistades neil otse teie aknaobjektiga suhelda.Cross-Origin-Embedder-Policy: require-corp
(COEP): Nõuab, et kõik teie lehe laaditud alamressursid (nagu pildid, skriptid ja iframe'id) peavad olema kas samast päritolust või selgesõnaliselt märgitud päritoluüleselt laetavaksCross-Origin-Resource-Policy
päise või CORS-i abil.
Selle seadistamine võib olla keeruline, eriti kui te toetute kolmandate osapoolte skriptidele või ressurssidele, mis ei paku vajalikke päiseid. Pärast serveri konfigureerimist saate kontrollida, kas teie leht on isoleeritud, kontrollides brauseri konsoolis omadust self.crossOriginIsolated
. See peab olema true
.
2. samm: Puhvri loomine ja jagamine
Oma põhi skriptis loote SharedArrayBuffer
'i ja sellele "vaate", kasutades TypedArray
'd nagu Int32Array
.
main.js:
// Kontrolli esmalt päritoluülest isoleerimist!
if (!self.crossOriginIsolated) {
console.error("See leht ei ole päritoluüleselt isoleeritud. SharedArrayBuffer ei ole saadaval.");
} else {
// Loo jagatud puhver ühe 32-bitise täisarvu jaoks.
const buffer = new SharedArrayBuffer(4);
// Loo puhvrile vaade. Kõik atomaarsed operatsioonid toimuvad vaatel.
const int32Array = new Int32Array(buffer);
// Initsialiseeri väärtus indeksil 0.
int32Array[0] = 0;
// Loo uus worker.
const worker = new Worker('worker.js');
// Saada JAGATUD puhver workerile. See on viite edastus, mitte koopia.
worker.postMessage({ buffer });
// Kuula sõnumeid workerilt.
worker.onmessage = (event) => {
console.log(`Worker teatas lõpetamisest. Lõplik väärtus: ${Atomics.load(int32Array, 0)}`);
};
}
3. samm: Atomaarsete operatsioonide sooritamine Workeris
Worker võtab puhvri vastu ja saab nüüd sellel atomaarseid operatsioone sooritada.
worker.js:
self.onmessage = (event) => {
const { buffer } = event.data;
const int32Array = new Int32Array(buffer);
console.log("Worker sai jagatud puhvri kätte.");
// Sooritame mõned atomaarsed operatsioonid.
for (let i = 0; i < 1000000; i++) {
// Suurenda turvaliselt jagatud väärtust.
Atomics.add(int32Array, 0, 1);
}
console.log("Worker lõpetas suurendamise.");
// Anna põhilõimele teada, et oleme valmis.
self.postMessage({ done: true });
};
4. samm: Keerukam näide – paralleelne summeerimine sünkroniseerimisega
Võtame ette realistlikuma probleemi: väga suure numbritemassiivi summeerimine, kasutades mitut workerit. Kasutame tõhusaks sünkroniseerimiseks Atomics.wait()
ja Atomics.notify()
.
Meie jagatud puhvril on kolm osa:
- Indeks 0: Olekulipp (0 = töötlemisel, 1 = valmis).
- Indeks 1: Loendur, mitu workerit on lõpetanud.
- Indeks 2: Lõplik summa.
main.js:
if (self.crossOriginIsolated) {
const NUM_WORKERS = 4;
const DATA_SIZE = 10_000_000;
// [olek, workerid_lõpetanud, tulemus_madal, tulemus_kõrge]
// Kasutame tulemuse jaoks kahte 32-bitist täisarvu, et vältida suurte summade puhul ületäitumist.
const sharedBuffer = new SharedArrayBuffer(4 * 4); // 4 täisarvu
const sharedArray = new Int32Array(sharedBuffer);
// Genereeri töötlemiseks juhuslikke andmeid
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);
// Loo workeri andmejada jaoks mitte-jagatud vaade
const dataChunk = data.subarray(start, end);
worker.postMessage({
sharedBuffer,
dataChunk // See kopeeritakse
});
}
console.log('Põhilõim ootab nüüd workerite lõpetamist...');
// Oota, kuni olekulipp indeksil 0 muutub 1-ks
// See on palju parem kui while-tsükkel!
Atomics.wait(sharedArray, 0, 0); // Oota, kui sharedArray[0] on 0
console.log('Põhilõim äratati!');
const finalSum = Atomics.load(sharedArray, 2);
console.log(`Lõplik paralleelne summa on: ${finalSum}`);
} else {
console.error('Leht ei ole päritoluüleselt isoleeritud.');
}
sum_worker.js:
self.onmessage = ({ data }) => {
const { sharedBuffer, dataChunk } = data;
const sharedArray = new Int32Array(sharedBuffer);
// Arvuta selle workeri andmejada summa
let localSum = 0;
for (let i = 0; i < dataChunk.length; i++) {
localSum += dataChunk[i];
}
// Lisa atomaarselt kohalik summa jagatud kogusummale
Atomics.add(sharedArray, 2, localSum);
// Suurenda atomaarselt 'workerid lõpetanud' loendurit
const finishedCount = Atomics.add(sharedArray, 1, 1) + 1;
// Kui see on viimane lõpetav worker...
const NUM_WORKERS = 4; // Tõelises rakenduses tuleks see parameetrina edasi anda
if (finishedCount === NUM_WORKERS) {
console.log('Viimane worker lõpetas. Teavitan põhilõime.');
// 1. Määra olekulipp 1-ks (valmis)
Atomics.store(sharedArray, 0, 1);
// 2. Teavita põhilõime, mis ootab indeksil 0
Atomics.notify(sharedArray, 0, 1);
}
};
Reaalse maailma kasutusjuhud ja rakendused
Kus see võimas, kuid keeruline tehnoloogia tegelikult midagi muudab? See paistab silma rakendustes, mis nõuavad rasket, paralleelistatavat arvutust suurtel andmekogumitel.
- WebAssembly (Wasm): See on peamine kasutusjuht. Keeled nagu C++, Rust ja Go omavad küpset tuge mitmelõimelisusele. Wasm võimaldab arendajatel kompileerida olemasolevaid suure jõudlusega mitmelõimelisi rakendusi (nagu mängumootorid, CAD-tarkvara ja teaduslikud mudelid) brauseris käitamiseks, kasutades
SharedArrayBuffer
'it lõimede vahelise suhtluse alusmehhanismina. - Brauserisisene andmetöötlus: Suuremahulist andmete visualiseerimist, kliendipoolset masinõppemudelite järeldamist ja teaduslikke simulatsioone, mis töötlevad tohutuid andmemahte, saab oluliselt kiirendada.
- Meedia redigeerimine: Filtrite rakendamist kõrge resolutsiooniga piltidele või helitöötlust helifailil saab jagada tükkideks ja töödelda paralleelselt mitme workeri poolt, pakkudes kasutajale reaalajas tagasisidet.
- Kõrge jõudlusega mängimine: Kaasaegsed mängumootorid toetuvad tugevalt mitmelõimelisusele füüsika, tehisintellekti ja varade laadimise jaoks.
SharedArrayBuffer
võimaldab luua konsoolikvaliteediga mänge, mis töötavad täielikult brauseris.
Väljakutsed ja lõplikud kaalutlused
Kuigi SharedArrayBuffer
on transformatiivne, ei ole see hõbekuul. See on madala taseme tööriist, mis nõuab hoolikat käsitlemist.
- Keerukus: Konkurentne programmeerimine on kurikuulsalt raske. Võidujooksu tingimuste ja ummikseisude silumine võib olla uskumatult keeruline. Peate mõtlema teisiti sellele, kuidas teie rakenduse olekut hallatakse.
- Ummikseisud: Ummikseis (deadlock) tekib siis, kui kaks või enam lõime on igaveseks blokeeritud, kumbki oodates, et teine vabastaks ressursi. See võib juhtuda, kui te implementeerite keerukaid lukustusmehhanisme valesti.
- Turvalisuse lisakulu: Päritoluülese isoleerimise nõue on oluline takistus. See võib rikkuda integratsioone kolmandate osapoolte teenuste, reklaamide ja makseväravatega, kui need ei toeta vajalikke CORS/CORP päiseid.
- Mitte iga probleemi jaoks: Lihtsate taustaülesannete või I/O-operatsioonide jaoks on traditsiooniline Web Workeri mudel
postMessage()
'ga sageli lihtsam ja piisav. KasutageSharedArrayBuffer
'it ainult siis, kui teil on selge, protsessorist sõltuv kitsaskoht, mis hõlmab suuri andmemahtusid.
Kokkuvõte
SharedArrayBuffer
koos Atomics
'i ja Web Workeritega esindab paradigma muutust veebiarenduses. See purustab ühelõimelise mudeli piirid, kutsudes brauserisse uue klassi võimsaid, jõudsaid ja keerukaid rakendusi. See asetab veebiplatvormi võrdsemale alusele natiivsete rakenduste arendamisega arvutusmahukate ülesannete osas.
Teekond konkurentsesse JavaScripti on väljakutsuv, nõudes ranget lähenemist olekuhaldusele, sünkroniseerimisele ja turvalisusele. Kuid arendajatele, kes soovivad nihutada veebis võimaliku piire – alates reaalajas helisünteesist kuni keeruka 3D-renderdamise ja teadusliku andmetöötluseni – ei ole SharedArrayBuffer
'i valdamine enam lihtsalt valikuvõimalus; see on hädavajalik oskus järgmise põlvkonna veebirakenduste ehitamiseks.