JavaScript는 단일 스레드입니다: 실제로 의미하는 것

자바스크립트에는 단일 실행 스레드: 언제든지 하나만 JavaScript 문이 실행 중입니다. 이것은 런타임에 관한 사실이지 실제적인 사실이 아닙니다. 언어의 한계. JavaScript 엔진(Chrome/Node.js의 V8, Firefox의 SpiderMonkey) 코드를 순차적으로 실행합니다.

그러나 Node.js는 수만 개의 동시 요청을 처리합니다. 처럼? 통해 이벤트 루프: 비동기 I/O 작업을 기다릴 수 있는 메커니즘 (네트워크, 파일 시스템) 스레드를 차단하지 않고 실제 작업을 운영 체제에 위임합니다. 통해 libuv.

이벤트 루프의 구성요소

Node.js(V8 + libuv)의 이벤트 루프는 다음으로 구성됩니다.

  • 호출 스택: JavaScript 코드의 동기 실행 스택입니다. 언제 함수가 호출되면 스택에 "푸시"됩니다. 끝나면 "먹이"됩니다. 스택이 비어 있지 않으면 스레드가 사용 중입니다.
  • 웹 API/노드 API: 환경(브라우저 또는 Node.js)에서 제공하는 인터페이스 비동기 I/O 작업의 경우: setTimeout, fetch, fs.readFile, WebSocket 등. 이러한 작업은 C++ 런타임(libuv)에 위임되며 JS 스레드를 사용하지 않습니다.
  • 콜백 대기열(매크로태스크 대기열): 다음에 실행할 콜백 대기열 I/O 작업 완료, 시간 초과, 간격 호출 스택이 비어 있는 후에 실행됨 마이크로태스크 대기열은 비워집니다.
  • 마이크로태스크 대기열: Promise 콜백을 위한 높은 우선순위 대기열 (.then(), async/await) 그리고 queueMicrotask(). 그 사람이 온다 각 매크로 작업 전에 완전히 비워집니다.

실행 순서: 기본 규칙

이벤트 루프의 가장 중요한 규칙은 다음과 같습니다.

  1. 호출 스택에서 모든 동기 코드 실행
  2. 마이크로태스크 대기열을 완전히 지웁니다(Promise, MutationObserver).
  3. 실행 다음 매크로태스크 콜백 대기열에서(setTimeout, I/O 콜백)
  4. 포인트 2로 돌아가세요.

이는 많은 개발자를 놀라게 하는 동작을 설명합니다.

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.

이벤트 루프를 단계별로 살펴보세요

// 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

약속 및 비동기/대기: 세부 정보

async/await 그것은 약속 위에 구문 설탕입니다. 컴파일러는 변환합니다 모든 await in un .then(). 변환을 이해하면 이해하는 데 도움이 됩니다. 실행 순서:

// 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)

마이크로태스크 기아: 피해야 할 안티 패턴

마이크로태스크 대기열이 결코 비워지지 않으면 매크로태스크(따라서 I/O 콜백)도 비어 있지 않습니다. 수행한 적이 없습니다. 이것은 호출됩니다 마이크로태스크 기아:

// 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 이벤트 루프: 단계

Node.js 이벤트 루프에는 브라우저를 넘어서는 특정 단계(libuv)가 있습니다.

// 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
});

이벤트 루프 차단: 고전적인 실수

이벤트 루프를 차단하는 작업

  • 동기식 CPU 집약적 컴퓨팅: 2초 블록을 계산하는 루프 다른 모든 요청은 2초 동안 지속됩니다. CPU 바인딩 작업에는 작업자 스레드를 사용합니다.
  • 거대한 페이로드에 대한 JSON.parse(): 10MB JSON을 구문 분석하는 것은 동기식 및 블록. JSON 스트림을 사용하거나 작업자 스레드에 위임하세요.
  • 서버 코드의 fs.readFileSync(): 모든 Node.js API의 버전 동기화 그들은 스레드를 차단합니다. 항상 콜백과 함께 비동기 버전을 사용하거나 대기하세요.
  • 플래그가 없는 암호화 작업: 암호화 작업은 CPU에 바인딩됩니다. crypto.scrypt() 콜백을 수락합니다. crypto.scryptSync() 차단하다.

결론

이벤트 루프는 단일 스레드임에도 불구하고 JavaScript를 효율적으로 만드는 것입니다. 기억해야 할 핵심: 마이크로태스크 대기열은 매크로태스크 대기열보다 우선순위가 높습니다. await 스레드를 차단하지 않지만 현재 기능을 일시 중지합니다. CPU 바인딩된 동기 작업 블록 전체 이벤트 루프.

다음 기사에서는 고루틴과 Go 채널: 모델 CSP를 기반으로 하며 수백만 개의 작업으로 확장되도록 설계된 완전히 다른 동시성 플랫폼 경쟁자.

이전 기사 ← 경쟁 모델
시리즈의 다음 Go의 고루틴과 채널 →