Node.js-taustakehityksen bootcamp · Oppitunti

Muistin jakaminen SharedArrayBufferilla ja Atomicsilla

Koordinoi säikeitä jaetun muistin avulla Atomicsilla, jotta suuria puskureita ei tarvitse kopioida ja kilpailutilanteet vältetään.

Oppitunti 3/413 vaihetta

Muistin jakaminen SharedArrayBufferilla ja Atomicsilla on ilmainen Node.js-taustakehityksen bootcamp-oppitunti CoddyKitissä. Tämä on oppitunti 3/4. Voit lukea koko oppitunnin alta ilmaiseksi ja harjoitella sen jälkeen käytännössä selaimessa sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla. Oppitunti kuuluu Node.js-taustakehityksen bootcamp-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. Node.js-taustakehityksen bootcamp-kurssilla on yhteensä 4 oppituntia.

Miksi jaettu muisti?

Kun käynnistätte Worker-säikeen Node.js:ssä ja kutsutte postMessage-funktiota, lähettämänne data kopioidaan structured clone -algoritmilla. Pienille viesteille tämä sopii hyvin, mutta suuren numeerisen puskurin (miljoonia tavuja) kopiointi tuhlaa suorittimen aikaa ja muistia.

  • SharedArrayBuffer mahdollistaa sen, että useat säikeet lukevat ja kirjoittavat samaa muistilohkoa ilman kopiointia.
  • Atomics tarjoaa turvalliset, kilpailutilanteettomat operaatiot kyseiseen muistiin.

Tässä oppitunnissa opitte koordinoimaan säikeitä jaetun muistin avulla suoritinta kuormittavassa taustatehtävässä.

ArrayBuffer ja SharedArrayBuffer

ArrayBuffer kuuluu yhdelle säikeelle. Kun se siirretään workerille, lähettäjä menettää siihen pääsyn. SharedArrayBuffer (SAB) toimii eri tavalla: sen välittäminen workerille jakaa saman taustalla olevan tallennustilan, joten molemmat säikeet näkevät toistensa kirjoittamat arvot.

Raakavuja ei lueta suoraan. Sen sijaan puskuriin liitetään tyypitetty taulukko näkymäksi, esimerkiksi Int32Array tai Float64Array.

const sab = new SharedArrayBuffer(16);
const view = new Int32Array(sab);

console.log(view.length);
view[0] = 42;
console.log(view[0]);
console.log(sab.byteLength);

SAB:n välittäminen Workerille

Voitte jakaa muistia luomalla SharedArrayBuffer-olion pääsäikeessä ja lähettämällä sen postMessage-funktiolla. Tavallisesta puskurista poiketen SAB jaetaan (sitä ei siirretä), joten molemmat osapuolet voivat jatkaa sen käyttämistä.

Worker liittää saman SAB:n omaan tyypitetyn taulukon näkymäänsä. Kopiointia ei tapahdu.

const { Worker, isMainThread, workerData } = require('worker_threads');

if (isMainThread) {
  const sab = new SharedArrayBuffer(4);
  const view = new Int32Array(sab);
  view[0] = 100;
  new Worker(__filename, { workerData: sab });
} else {
  const view = new Int32Array(workerData);
  view[0] += 1;
  console.log('worker sees', view[0]);
}

Kilpailutilanteen ongelma

Jaetun näkymän tavalliset luku- ja kirjoitusoperaatiot eivät ole turvallisia, kun useat säikeet käsittelevät samaa paikkaa. Lauseke view[0] += 1 koostuu todellisuudessa kolmesta vaiheesta: lue, lisää ja kirjoita. Kaksi säiettä voi suorittaa vaiheet lomittain, jolloin päivityksiä menetetään.

  • Säie A lukee arvon 5, ja säie B lukee arvon 5.
  • Molemmat laskevat arvoksi 6 ja kirjoittavat arvon 6.
  • Lisäyksiä tehtiin kaksi, mutta arvo kasvoi vain yhdellä.

Tämä on tyypillinen datarace. Ratkaisu on Atomics.

Atomics.add turvallisia laskureita varten

Atomics.add(view, index, value) suorittaa lue-muokkaa-kirjoita-operaation yhtenä jakamattomana operaationa. Mikään muu säie ei voi suorittaa operaatiota lomittain, joten lisäyksiä ei koskaan menetetä.

Muita hyödyllisiä metodeja ovat Atomics.sub, Atomics.and, Atomics.or sekä Atomics.load / Atomics.store tavallisiin luku- ja kirjoitusoperaatioihin, joiden näkyvyys säikeiden välillä taataan.

const sab = new SharedArrayBuffer(4);
const counter = new Int32Array(sab);

Atomics.store(counter, 0, 0);
Atomics.add(counter, 0, 5);
Atomics.add(counter, 0, 3);

console.log(Atomics.load(counter, 0));

Suoritintyön jakaminen säikeille

Kuvitelkaa valtavan kokonaislukutaulukon summaaminen: kyseessä on suoritinta kuormittava tehtävä, joka estäisi tapahtumasilmukan toiminnan. Jaetun muistin avulla tallennatte datan kerran ja annatte useiden workereiden käsitellä kukin omaa osuuttaan sekä kirjoittaa osatulokset jaettuun tulospaikkaan Atomics.add-operaatiolla.

Koska syöte sijaitsee SharedArrayBuffer-puskurissa, aineistoa ei tarvitse kopioida jokaiselle workerille. Kaikki lukevat samoja tavuja.

Vertailu ja vaihto

Atomics.compareExchange(view, index, expected, replacement) kirjoittaa arvon replacement vain, jos nykyinen arvo on sama kuin expected, ja palauttaa paikalla aiemmin olleen arvon. Tämä on lukottomien algoritmien ja yksinkertaisten spinlockien perusrakennusosa.

Sen avulla voitte varata paikan täsmälleen kerran: jos vaihto onnistuu, tämä säie voitti varauksen.

const sab = new SharedArrayBuffer(4);
const slot = new Int32Array(sab);
Atomics.store(slot, 0, 0);

const prev = Atomics.compareExchange(slot, 0, 0, 1);
console.log('previous value was', prev);
console.log('claimed:', prev === 0);

const again = Atomics.compareExchange(slot, 0, 0, 1);
console.log('second claim succeeded:', again === 0);

Estäminen Atomics.wait-operaatiolla

Joskus workerin on odotettava, kunnes toinen säie ilmoittaa sille. Atomics.wait(view, index, expectedValue) estää kutsuvan säikeen toiminnan niin kauan kuin paikassa on arvo expectedValue. Se palauttaa arvon 'ok', 'not-equal' tai 'timed-out'.

  • Atomics.wait toimii vain pääsäikeen ulkopuolella (muuten se jäädyttäisi tapahtumasilmukan).
  • Atomics.notify(view, index, count) herättää odottavat säikeet.

Näin saatte todellisen säikeiden välisen esteen ilman aktiivista silmukointia.

Ilmoitus workereiden herättämiseksi

Tuottajasäie päivittää jaetun paikan Atomics.store-operaatiolla ja kutsuu sen jälkeen Atomics.notify-operaatiota herättääkseen säikeet, jotka ovat jääneet odottamaan Atomics.wait-operaatiolla. Järjestyksellä on merkitystä: muuttakaa arvo ensin ja ilmoittakaa vasta sen jälkeen.

Oikeassa taustatehtävässä koordinaattori vapauttaa näin kaikki workerit yhtä aikaa aloittamaan vaiheen tai ilmoittaa syötteen olevan valmis.

const { Worker, isMainThread, workerData } = require('worker_threads');

if (isMainThread) {
  const sab = new SharedArrayBuffer(4);
  const signal = new Int32Array(sab);
  Atomics.store(signal, 0, 0);
  new Worker(__filename, { workerData: sab });
  setTimeout(() => {
    Atomics.store(signal, 0, 1);
    Atomics.notify(signal, 0, 1);
  }, 50);
} else {
  const signal = new Int32Array(workerData);
  Atomics.wait(signal, 0, 0);
  console.log('worker released, value =', Atomics.load(signal, 0));
}

Täydellinen rinnakkainen summa

Tässä on koko toimintamalli yhdessä suoritettavassa tiedostossa: jaettu syötepuskuri, jaettu tulospaikka ja kaksi workeria, jotka summaavat kumpikin puolet datasta ja lisäävät osatuloksensa atomisesti tulokseen. Pääsäie odottaa, että molemmat valmistuvat.

Huomaatte, ettei syötettä koskaan kopioida; molemmat workerit lukevat samaa SharedArrayBuffer-puskuria.

const { Worker, isMainThread, workerData } = require('worker_threads');

if (isMainThread) {
  const N = 1000;
  const dataSab = new SharedArrayBuffer(N * 4);
  const data = new Int32Array(dataSab);
  for (let i = 0; i < N; i++) data[i] = i + 1;

  const resultSab = new SharedArrayBuffer(8);
  const result = new Int32Array(resultSab);
  Atomics.store(result, 0, 0);
  Atomics.store(result, 1, 0);

  let done = 0;
  const ranges = [[0, N / 2], [N / 2, N]];
  for (const [start, end] of ranges) {
    const w = new Worker(__filename, { workerData: { dataSab, resultSab, start, end } });
    w.on('exit', () => {
      if (++done === ranges.length) {
        console.log('total =', Atomics.load(result, 0));
      }
    });
  }
} else {
  const { dataSab, resultSab, start, end } = workerData;
  const data = new Int32Array(dataSab);
  const result = new Int32Array(resultSab);
  let local = 0;
  for (let i = start; i < end; i++) local += data[i];
  Atomics.add(result, 0, local);
}

Käytännön huomioita

Jaettu muisti on tehokas, mutta sitä on helppo käyttää väärin. Pitäkää seuraavat säännöt mielessä:

  • SharedArrayBuffer tallentaa vain numeroita. Merkkijonojen tai olioiden jakamiseksi ne on koodattava (esimerkiksi TextEncoder-oliolla Uint8Array-taulukoksi).
  • Käyttäkää aina Atomics-operaatioita paikoissa, joihin useampi kuin yksi säie saattaa kirjoittaa; tavallisten ja atomisten kirjoitusten sekoittaminen tuo kilpailutilanteet takaisin.
  • Varatkaa kiinteä paikka synkronointilipuille ja dokumentoikaa sen indeksi.
  • Käyttäkää jaettua muistia vain, kun kopiointi on todellinen pullonkaula; useimmille viesteille tavallinen postMessage on yksinkertaisempi ja turvallinen.

Pikatarkistus

Teillä on useita worker-säikeitä, jotka kasvattavat yhtä jaettua laskuria. Laskuri on SharedArrayBuffer-puskurin tukemassa Int32Array-taulukossa. Mikä tapa pitää laskurin arvon oikeana samanaikaisessa käytössä?

Kertaus

Opitte koordinoimaan Node.js:n worker-säikeitä jaetun muistin avulla:

  • SharedArrayBuffer jakaa yhden taustalla olevan tallennustilan säikeiden kesken ilman kopiointia; liittäkää siihen tyypitetty taulukko, kuten Int32Array.
  • Jaettuun paikkaan kohdistuva tavallinen += aiheuttaa dataraceja; käyttäkää Atomics-operaatioita paikoissa, joihin kirjoitetaan samanaikaisesti.
  • Atomics.add, Atomics.load, Atomics.store ja Atomics.compareExchange tarjoavat kilpailutilanteettomat luku- ja kirjoitusoperaatiot sekä lukottomat varaukset.
  • Atomics.wait (pääsäikeen ulkopuolella) yhdessä Atomics.notify-operaation kanssa mahdollistaa säikeiden estämisen ja ilmoitusten lähettämisen ilman aktiivista silmukointia.
  • Käyttäkää jaettua muistia vasta, kun suurten numeeristen puskureiden kopiointi on todellinen pullonkaula.
Aloita maksutta

Opi JavaScript tekoälytuutorin avulla — ilmaiseksi

Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.

Kurssit
22
Oppitunnit
92

Usein kysytyt kysymykset

Onko oppitunti ”Muistin jakaminen SharedArrayBufferilla ja Atomicsilla” ilmainen?

Kyllä – oppitunnin ”Muistin jakaminen SharedArrayBufferilla ja Atomicsilla” koko tekstin voi lukea täällä verkossa ilmaiseksi. Jos haluat harjoitella interaktiivisesti sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla sekä avata koko Node.js-taustakehityksen bootcamp-kurssin, päivitä CoddyKit PROhon. Node.js-taustakehityksen bootcamp-kurssilla on yhteensä 4 oppituntia.

Mitä opin oppitunnilla ”Muistin jakaminen SharedArrayBufferilla ja Atomicsilla”?

Koordinoi säikeitä jaetun muistin avulla Atomicsilla, jotta suuria puskureita ei tarvitse kopioida ja kilpailutilanteet vältetään. Harjoittelet Node.js-taustakehityksen bootcamp-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.

Tarvitsenko kokemusta aloittaakseni Node.js-taustakehityksen bootcamp-opiskelun?

Aiempi kokemus ei ole tarpeen. CoddyKitin Node.js-taustakehityksen bootcamp-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 3/4.

Kuinka kauan ”Muistin jakaminen SharedArrayBufferilla ja Atomicsilla”-oppitunnin suorittaminen kestää?

Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.

Voinko kirjoittaa ja suorittaa koodia tällä Node.js-taustakehityksen bootcamp-oppitunnilla?

Kyllä. Jokainen Node.js-taustakehityksen bootcamp-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.

Kaikki tämän kurssin oppitunnit

  1. Miksi tapahtumasilmukka pysähtyy suorittimen kuormittuessa
  2. Worker-säikeiden käynnistäminen ja viestien välittäminen
  3. Muistin jakaminen SharedArrayBufferilla ja Atomicsilla
  4. Uudelleenkäytettävän worker-poolin rakentaminen suorituskykyä varten
← Takaisin: Node.js-taustakehityksen bootcamp