Bootcamp i backendutveckling med Node.js · Lektion

Varför händelseslingan stannar vid CPU-bundet arbete

Identifiera blockerande beräkningar och förstå varför entrådad JavaScript behöver riktiga trådar för parallellism.

Lektion 1 av 413 steg

Varför händelseslingan stannar vid CPU-bundet arbete är en gratis lektion i Bootcamp i backendutveckling med Node.js på CoddyKit. Detta är lektion 1 av 4. Ni kan läsa hela lektionen gratis nedan och sedan öva praktiskt i webbläsaren med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt. Den ingår i lärvägen för Bootcamp i backendutveckling med Node.js, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Bootcamp i backendutveckling med Node.js innehåller totalt 4 lektioner.

En tråd för att köra allt

Node.js kör din JavaScript på en enda tråd. Alla begärandehanterare, timer-callbacks och fortsättningar från promises turas om att köras i den tråden, samordnade av händelseloopen.

Den här designen är utmärkt för I/O-bundet arbete: medan du väntar på en databassökning eller ett HTTP-svar är tråden fri att hantera andra begäranden. Väntan sker någon annanstans (i operativsystemet och libuv:s trådpool), inte i din JavaScript.

Men det finns en hake. Om en enskild callback börjar utföra tung beräkning är den enda tråden upptagen med att räkna, och inget annat kan köras förrän den är klar. I den här lektionen undersöker vi exakt varför CPU-bundet arbete blockerar händelseloopen.

Händelseloopen i en enda bild

Händelseloopen är en enkel idé: en loop som upprepade gånger hämtar nästa färdiga callback från en kö och kör den till avslut innan nästa hämtas.

  • En timer löper ut → kör dess callback.
  • En socket har data → kör dess callback.
  • En promise uppfylls → kör dess fortsättning.

Det viktiga är att callbacks är samarbetsbaserade: loopen kan inte avbryta en callback mitt i körningen. JavaScript har ingen preemption. Varje callback måste frivilligt returnera för att ge loopen möjlighet att göra något annat.

const server = require('http').createServer((req, res) => {
  // This callback must RETURN quickly so the loop can serve
  // the next request. The loop won't interrupt it midway.
  res.end('ok');
});

server.listen(3000, () => {
  console.log('Listening on http://localhost:3000');
});

I/O-bundet kontra CPU-bundet

För att förstå blockeringen måste vi först klassificera arbetet:

  • I/O-bundet: den mesta tiden går åt till att vänta på en extern resurs – disk, nätverk eller databas. CPU:n är mestadels inaktiv.
  • CPU-bundet: den mesta tiden går åt till att beräkna – hashning, bildskalning, tolkning av enorm JSON, komprimering, kryptografi eller stora loopar. CPU:n är belastad till 100 %.

Nodes modell med en tråd fungerar utmärkt för I/O-bundna arbetsbelastningar eftersom väntan inte upptar tråden. Den har svårt för CPU-bundna arbetsbelastningar eftersom beräkning upptar tråden – och det finns bara en.

Asynkron I/O blockerar inte

Asynkron I/O är icke-blockerande just eftersom den långa väntan delegeras. När du anropar en asynkron filläsning lämnar Node över arbetet till libuv (och operativsystemet), registrerar en callback och återlämnar omedelbart kontrollen till händelseloopen.

Loopen kan hantera tusentals andra saker medan disken arbetar. När läsningen är klar läggs din callback i kön. Tråden satt aldrig sysslolös och väntade.

Detta är den centrala insikten: att invänta I/O kostar inget för händelseloopen. Tråden frigörs för att utföra annat arbete.

const fs = require('fs/promises');

async function main() {
  console.log('before read');
  // Control returns to the loop while the disk works.
  const data = await fs.readFile(__filename, 'utf8');
  console.log('read', data.length, 'bytes');
}

main();
console.log('this prints BEFORE the read finishes');

En CPU-bunden funktion som blockerar

Jämför nu detta med beräkning. En tät loop som summerar en miljard tal väntar inte på något – den håller tråden fullt upptagen från början till slut.

Medan heavyCompute() körs är händelseloopen frusen. Inga timers körs, inga inkommande begäranden tas emot och inga fortsättningar från promises körs. Hela processen verkar ha hängt sig tills funktionen returnerar.

Kör detta och lägg märke till att programmet inte skriver ut något alls förrän loopen är klar – det finns ingen punkt där kontrollen återgår till loopen mitt under beräkningen.

function heavyCompute() {
  let total = 0;
  for (let i = 0; i < 2_000_000_000; i++) {
    total += i;
  }
  return total;
}

console.time('compute');
const result = heavyCompute();
console.timeEnd('compute');
console.log('result =', result);

Bevis: timers svälter under beräkning

Här är en direkt demonstration av en blockering. Vi schemalägger en timer på 100 ms och startar sedan omedelbart en CPU-bunden loop som tar mycket längre tid.

Ni kanske förväntar er att timern ska köras efter 100 ms. Det gör den inte. Loopen är upptagen med beräkningar och kan inte avbryta sig själv för att köra timerns callback. Timern körs först efter att beräkningen är klar — ofta flera sekunder för sent.

Detta är event loop-blockering i sin renaste form: en köad callback som är redo att köras men inte får tillgång till tråden.

const start = Date.now();

setTimeout(() => {
  console.log('Timer fired after', Date.now() - start, 'ms (asked for 100)');
}, 100);

// Block the single thread for ~2 seconds.
const end = Date.now() + 2000;
while (Date.now() < end) {
  // busy-wait, no I/O, no yielding
}
console.log('Blocking loop done after', Date.now() - start, 'ms');

async/await hjälper INTE CPU-arbete

En vanlig missuppfattning är att CPU-arbete blir icke-blockerande om det omsluts av en async-funktion eller om man lägger till await. Så är det inte.

async/await lämnar bara över tråden vid en faktisk await-punkt som pausar körningen vid en riktig asynkron åtgärd (I/O, en timer eller en microtask). En ren beräkning har ingen sådan pauspunkt — den körs synkront oavsett nyckelordet async.

I kodexemplet ändrar det ingenting att märka funktionen med async: loopen blockerar fortfarande under hela sin körning.

async function compute() {
  let total = 0;
  // No await inside a hot loop = still fully synchronous.
  for (let i = 0; i < 2_000_000_000; i++) {
    total += i;
  }
  return total;
}

setTimeout(() => console.log('timer wanted at 50ms'), 50);

console.time('compute');
compute().then((r) => {
  console.timeEnd('compute');
  console.log('result =', r);
});

Varför detta förstör en backend

På en server innebär en blockerad tråd att alla samtidiga klienter drabbas. Medan en begäran kör en 3 sekunder lång CPU-uppgift får alla andra begäranden som ligger i kö på den tråden vänta i hela 3 sekunder innan de ens läses.

  • Fördröjningen ökar kraftigt för orelaterade endpoints.
  • Hälsokontrollernas pingar når timeout, och orkestrerare kan avsluta processen eftersom den verkar vara "svarslös".
  • Genomströmningen kollapsar: en kärna, en uppgift i taget.

Servern har inte kraschat — den är helt enkelt monopoliserad. Därför kan en enda tung synkron handler slå ut en hel Node-tjänst under belastning.

const http = require('http');

http.createServer((req, res) => {
  if (req.url === '/heavy') {
    let t = 0;
    for (let i = 0; i < 5_000_000_000; i++) t += i; // blocks everyone
    return res.end('done ' + t);
  }
  // /ping cannot respond while /heavy is running on the same thread
  res.end('pong');
}).listen(3000);

Uppdelning hjälper lite, men inte tillräckligt

En delvis lösning är att dela upp arbetet i delar och köra setImmediate mellan dem, så att eventloopen får andrum mellan delarna.

Detta håller servern responsiv — pingar kan besvaras mellan delarna — men det skapar inte parallellism. Beräkningen körs fortfarande på den enda tråden, nu varvad med andra callbackar, så den totala väggklockstiden för den tunga uppgiften blir vanligtvis längre, inte kortare.

Uppdelning byter minskad fördröjning för andra mot genomströmning för uppgiften. Den kan inte använda en andra CPU-kärna. För äkta parallellism behöver ni riktiga trådar.

function computeChunked(total, i, end, done) {
  const sliceEnd = Math.min(i + 10_000_000, end);
  for (; i < sliceEnd; i++) total += i;
  if (i < end) {
    // Yield to the loop, then continue next tick.
    setImmediate(() => computeChunked(total, i, end, done));
  } else {
    done(total);
  }
}

computeChunked(0, 0, 2_000_000_000, (r) => console.log('result =', r));
setInterval(() => console.log('loop still alive'), 200);

Den riktiga lösningen: flytta arbetet från huvudtråden

Eftersom JavaScript på huvudtråden är entrådigt och kooperativt är det enda sättet att köra CPU-bundet arbete parallellt — med fler än en CPU-kärna — att flytta det till en separat körningstråd.

Node ger er flera alternativ:

  • Worker Threads (worker_threads): riktiga OS-trådar i samma process, där varje tråd har sin egen eventloop och V8-isolat. Idealiskt för CPU-bundna uppgifter.
  • Cluster-/barnprocesser: separata processer med större isolering och högre overhead.

Huvudtråden lämnar över det tunga arbetet, förblir responsiv för I/O och hämtar resultatet via ett meddelande när workern är klar.

En workertråd håller eventloopen fri

Så här ser lösningen ut i stora drag. Huvudtråden skapar en Worker som kör den tunga beräkningen på en annan tråd och en annan kärna. Huvudeventloopen förblir fri att hantera begäranden och svara på timerhändelser.

När workern är klar skickar den tillbaka ett meddelande. Huvudtrådens callback kör resultatet genom eventloopen — icke-blockerande, parallellt och skalbart över flera kärnor.

Detta är grunden för CPU-bunden parallellism i Node, som resten av kursen bygger vidare på.

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

if (isMainThread) {
  const worker = new Worker(__filename);
  worker.on('message', (sum) => console.log('worker result =', sum));

  // Main loop is NOT blocked: this timer fires on time.
  setInterval(() => console.log('main thread responsive'), 200);
} else {
  let total = 0;
  for (let i = 0; i < 2_000_000_000; i++) total += i;
  parentPort.postMessage(total);
}

Snabb kontroll

En Node HTTP-handler kör en synkron loop för bildhashning som tar 4 sekunder. Vad händer under dessa 4 sekunder med en andra klient som anropar en annan, lätt endpoint?

Sammanfattning

Viktiga lärdomar från lektionen:

  • Node kör JavaScript på en enda kooperativ eventloop-tråd — callbackar körs tills de är klara och kan inte avbrytas.
  • I/O-bundet arbete är icke-blockerande eftersom väntan delegeras till libuv/OS; tråden frigörs.
  • CPU-bundet arbete upptar tråden under hela körningen och fryser timers, begäranden och fortsättningar av promises — eventloopen blockeras.
  • async/await hjälper inte vid rena beräkningar; det lämnar bara över tråden vid riktiga asynkrona pausunkter.
  • Uppdelning med setImmediate återställer responsiviteten men tillför ingen parallellism och kan inte använda extra kärnor.
  • Den riktiga lösningen är Worker Threads: flytta CPU-bundet arbete till en separat tråd/kärna så att huvudloopen förblir fri.
Gratis att börja

Lär dig JavaScript med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
22
Lektioner
92

Vanliga frågor

Är lektionen ”Varför händelseslingan stannar vid CPU-bundet arbete” gratis?

Ja – hela texten till ”Varför händelseslingan stannar vid CPU-bundet arbete” kan läsas gratis här på webben. Om Ni vill öva interaktivt med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt och låsa upp resten av kursen i Bootcamp i backendutveckling med Node.js, kan Ni uppgradera till CoddyKit PRO. Kursen i Bootcamp i backendutveckling med Node.js innehåller totalt 4 lektioner.

Vad lär jag mig i ”Varför händelseslingan stannar vid CPU-bundet arbete”?

Identifiera blockerande beräkningar och förstå varför entrådad JavaScript behöver riktiga trådar för parallellism. Ni övar på Bootcamp i backendutveckling med Node.js med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig Bootcamp i backendutveckling med Node.js?

Du behöver inga förkunskaper. Utbildningen i Bootcamp i backendutveckling med Node.js på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 1 av 4.

Hur lång tid tar lektionen ”Varför händelseslingan stannar vid CPU-bundet arbete”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här Bootcamp i backendutveckling med Node.js-lektionen?

Ja. Varje Bootcamp i backendutveckling med Node.js-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Varför händelseslingan stannar vid CPU-bundet arbete
  2. Skapa worker-trådar och skicka meddelanden
  3. Dela minne med SharedArrayBuffer och Atomics
  4. Bygg en återanvändbar worker-pool för genomströmning
← Tillbaka till Bootcamp i backendutveckling med Node.js