JavaScript is single-threaded: wat het werkelijk betekent

JavaScript heeft één enkele uitvoeringsdraad: op elk moment slechts één JavaScript-instructie wordt uitgevoerd. Dit is een runtime feit, niet een grens van de taal. De JavaScript-engine (V8 in Chrome/Node.js, SpiderMonkey in Firefox) voert de code opeenvolgend uit.

Toch verwerkt Node.js tienduizenden gelijktijdige verzoeken. Als? Via de gebeurtenis lus: een mechanisme waarmee u kunt wachten op asynchrone I/O-bewerkingen (netwerk, bestandssysteem) zonder de thread te blokkeren, waardoor het daadwerkelijke werk aan het besturingssysteem wordt gedelegeerd via libuv.

De componenten van de gebeurtenislus

De gebeurtenislus in Node.js (V8 + libuv) bestaat uit:

  • Stapels bellen: De synchrone uitvoeringsstapel van JavaScript-code. Wanneer er wordt een functie aangeroepen, deze wordt op de stapel "geduwd"; als het klaar is, wordt het "gevoed". Als de stapel niet leeg is, is de thread bezet.
  • Web-API's/knooppunt-API's: interfaces geleverd door de omgeving (browser of Node.js) voor asynchrone I/O-bewerkingen: setTimeout, fetch, fs.readFile, WebSockets, enz. Deze bewerkingen worden gedelegeerd aan de C++-runtime (libuv) en maken GEEN gebruik van de JS-thread.
  • Terugbelwachtrij (macrotakenwachtrij): Wachtrij met callbacks die vervolgens moeten worden uitgevoerd I/O-bewerkingen voltooid, time-out, interval Uitgevoerd NADAT de call-stack leeg is en de wachtrij voor microtaken wordt geleegd.
  • Microtask-wachtrij: wachtrij met hoge prioriteit voor Promise-terugbelgesprekken (.then(), async/await) En queueMicrotask(). Hij komt volledig leeggemaakt VOOR elke macrotaak.

De volgorde van uitvoering: de fundamentele regel

De belangrijkste regel van de gebeurtenislus:

  1. Voer alle synchrone code uit op de call-stack
  2. Wis de wachtrij voor microtaken volledig (Promise, MutationObserver)
  3. Voer de volgende macrotaak vanuit de terugbelwachtrij (setTimeout, I/O-terugbel)
  4. Ga terug naar punt 2

Dit verklaart gedrag dat veel ontwikkelaars verrast:

console.log('1 - sincrono');

setTimeout(() => console.log('4 - macrotask'), 0);

Promise.resolve()
    .then(() => console.log('2 - microtask 1'))
    .then(() => console.log('3 - microtask 2'));

console.log('1b - sincrono');

// Output:
// 1 - sincrono
// 1b - sincrono
// 2 - microtask 1
// 3 - microtask 2
// 4 - macrotask

// Perché? Il setTimeout con delay=0 è comunque un macrotask.
// Le Promise sono microtask, eseguite PRIMA del prossimo macrotask.

Bekijk de gebeurtenisloop stap voor stap

// Esempio completo — traccia mentale dell'esecuzione
async function main() {
    console.log('A');                        // [1] push main, push log('A')

    setTimeout(() => console.log('B'), 0);   // [2] delega a Web APIs

    await Promise.resolve('resolved');       // [3] sospende main, schedula microtask

    console.log('C');                        // [6] riprende dopo microtask
}

console.log('D');                            // [4] esegue sincrono

main();                                      // [1] chiama main

console.log('E');                            // [5] esegue sincrono dopo main si sospende

// Esecuzione:
// [1] D (sincrono prima di main)
// [2] A (main inizia, prima console.log)
// [3] main si sospende su await
// [4] E (codice sincrono dopo main())
// [5] Stack vuoto, microtask queue: Promise.resolve callback
// [6] C (main riprende dopo await)
// [7] Stack vuoto, macrotask queue: setTimeout callback
// [8] B

// Output finale: D, A, E, C, B

Beloof en async/wacht: Under the Hood

async/await het is syntactische suiker bovenop de Beloften. De compiler transformeert elke await in een .then(). Het begrijpen van de transformatie helpt om het te begrijpen de volgorde van uitvoering:

// Questa funzione async:
async function processUser(id) {
    const user = await fetchUser(id);         // await punto 1
    const orders = await fetchOrders(user);   // await punto 2
    return { user, orders };
}

// È equivalente a:
function processUserWithPromises(id) {
    return fetchUser(id)
        .then(user => {
            return fetchOrders(user)
                .then(orders => { return { user, orders }; });
        });
}

// La funzione si "sospende" a ogni await e
// riprende quando la Promise risolve (tramite microtask queue)

Verhongering door microtaken: een antipatroon dat u moet vermijden

Als de wachtrij voor microtaken nooit wordt geleegd, worden de macrotaken (en dus de I/O-callbacks) niet uitgevoerd. nooit uitgevoerd. Dit wordt genoemd honger door microtaken:

// ANTI-PATTERN: loop infinito di microtask — blocca tutto!
function recursiveMicrotask() {
    Promise.resolve().then(recursiveMicrotask);  // schedula infiniti microtask
}
recursiveMicrotask();
// setTimeout di seguito non verrà MAI eseguito!

// PATTERN CORRETTO: usa setImmediate (Node.js) o setTimeout per cedere il controllo
function processLargeDataset(data, index = 0) {
    if (index >= data.length) return;

    processItem(data[index]);

    // Cede il controllo all'event loop ogni 100 elementi
    if (index % 100 === 0) {
        setImmediate(() => processLargeDataset(data, index + 1));
    } else {
        processLargeDataset(data, index + 1);
    }
}

Node.js-gebeurtenislus: de fasen

De Node.js-gebeurtenislus heeft specifieke fasen (libuv) die verder gaan dan de browser:

// Le fasi del Node.js event loop (semplificato)
// 1. timers: esegue callback di setTimeout e setInterval
// 2. I/O callbacks: callback I/O differite (errori socket)
// 3. idle, prepare: uso interno Node.js
// 4. poll: recupera I/O events, esegue callback I/O
// 5. check: esegue callback di setImmediate()
// 6. close callbacks: es. socket.on('close', callback)

// setImmediate vs setTimeout(fn, 0): NON garantiti nell'ordine
// dipende dal momento di chiamata nel ciclo

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));

// Output non deterministico se eseguiti nel main module
// Ma dentro una callback I/O:
const fs = require('fs');
fs.readFile(__filename, () => {
    setTimeout(() => console.log('timeout'), 0);
    setImmediate(() => console.log('immediate'));
    // Qui 'immediate' è sempre PRIMA — siamo già nella fase poll
});

De gebeurtenislus blokkeren: klassieke fouten

Bewerkingen die de gebeurtenislus blokkeren

  • Synchronisch CPU-intensief computergebruik: een lus die blokken van 2 seconden berekent alle andere verzoeken gedurende 2 seconden. Gebruik Worker Threads voor CPU-gebonden werk.
  • JSON.parse() voor enorme payloads: het parseren van een JSON van 10 MB is synchroon en blokken. Gebruik JSON-streams of delegeer naar Worker Thread.
  • fs.readFileSync() in servercode: Synchroniseer versies van alle Node.js API's ze blokkeren de draad. Gebruik altijd asynchrone versies met callbacks of wacht.
  • Crypto-operaties zonder vlaggen: Cryptografische bewerkingen zijn CPU-gebonden. crypto.scrypt() terugbelverzoeken accepteren; crypto.scryptSync() blok.

Conclusies

De gebeurtenislus is wat JavaScript efficiënt maakt, ondanks dat het single-threaded is. De sleutels om te onthouden: de wachtrij voor microtaken heeft voorrang op de wachtrij voor macrotaken; await blokkeert de thread niet, maar schort de huidige functie op; CPU-gebonden synchrone bewerkingsblokken de hele gebeurtenislus.

In het volgende artikel zullen we de goroutines en Go-kanalen: een model compleet ander gelijktijdigheidsplatform, gebaseerd op CSP en ontworpen om te schalen naar miljoenen taken concurrenten.

Vorig artikel ← Competitiemodellen
Volgende in de serie Goroutine en Kanaal in Go →