Eesti

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:

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:

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.

  1. Lõim A loeb hetkeväärtuse, mis on 5.
  2. Enne kui Lõim A saab uue väärtuse kirjutada, peatab operatsioonisüsteem selle ja lülitub Lõimele B.
  3. Lõim B loeb hetkeväärtuse, mis on endiselt 5.
  4. Lõim B arvutab uue väärtuse (6) ja kirjutab selle tagasi mällu.
  5. 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.

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.

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

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:

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.

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.

  1. 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.
  2. 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.
  3. 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.
  4. Mitte iga probleemi jaoks: Lihtsate taustaülesannete või I/O-operatsioonide jaoks on traditsiooniline Web Workeri mudel postMessage()'ga sageli lihtsam ja piisav. Kasutage SharedArrayBuffer'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.