[Node.js] Thread Pool

Main·2024년 9월 29일
0

Node.js

목록 보기
9/20
post-thumbnail

Thread Pool비동기 작업을 효율적으로 처리하기 위한 중요한 개념입니다. Node.js는 싱글 스레드로 동작하는 이벤트 기반 환경이지만, I/O 작업이나 CPU 집중 작업을 효율적으로 처리하기 위해 백그라운드에서 여러 스레드를 사용하는 방식을 채택하고 있습니다. 이 스레드 풀(Thread Pool)은 libuv 라이브러리에 의해 관리됩니다.


Node.js와 싱글 스레드

Node.js는 자바스크립트의 싱글 스레드 모델을 따르며, 메인 이벤트 루프에서 자바스크립트 코드를 처리합니다. 하지만, 파일 시스템, 네트워크, 암호화 작업 등과 같은 I/O 작업을 비동기적으로 처리할 때, 스레드를 사용하지 않고도 효율적으로 실행됩니다. 이러한 비동기 작업 중 일부는 스레드 풀을 통해 병렬로 처리됩니다.


Thread Pool의 역할

Thread Pool은 Node.js에서 특정 작업을 비동기적으로 처리할 수 있도록 돕는 멀티 스레드 환경입니다. 주로 I/O 바운드 작업이나 CPU 집약적인 작업을 처리할 때 사용됩니다. 기본적으로 Node.js는 4개의 스레드로 구성된 풀을 사용하며, 이를 통해 자원 효율성을 높이고 동시 처리를 가능하게 합니다.

Node.js의 Thread Pool이 사용되는 주요 작업은 다음과 같습니다:

  1. 파일 시스템 작업 (File I/O):
    • 파일을 읽거나 쓸 때, 비동기적으로 처리하기 위해 백그라운드에서 스레드를 사용합니다.
  2. DNS 조회:
    • DNS 관련 작업도 네트워크를 통해 비동기적으로 처리되며, 스레드 풀이 사용될 수 있습니다.
  3. 암호화 및 압축 작업:
    • CPU 집약적인 작업(예: 암호화/해싱 또는 압축/해제 작업)은 스레드 풀에서 처리됩니다.
  4. 사용자 정의 비동기 작업:
    • Worker Threads 모듈을 사용하면 스레드 풀을 직접 활용하여 복잡한 작업을 처리할 수 있습니다.

Thread Pool 동작 원리

  1. top-level 코드 실행 : 콜백 함수 안에 있지 않은 모든 코드인 top-level 코드들이 실행됩니다.
  2. 이벤트 등록 : 서버 어플리케이션이 필요로 하는 모든 모듈들이 require되고 모든 콜백 이벤트가 등록됩니다.
  3. 요청 수신: 이벤트 루프가 비동기 작업(예: 파일 읽기)을 요청받으면, 즉시 해당 작업을 스레드 풀로 넘깁니다.
  4. 작업 할당: Thread Pool은 요청된 작업을 가능한 스레드 중 하나에 할당합니다. 기본적으로 Node.js는 4개의 스레드를 가집니다.
  5. 작업 실행: 스레드가 할당된 작업을 실행하고, 완료되면 결과를 메인 이벤트 루프로 반환합니다.
  6. 콜백 실행: 이벤트 루프는 반환된 결과를 처리하며, 등록된 콜백 함수가 실행됩니다.

Thread Pool의 크기 조정

기본적으로 Node.js의 스레드 풀은 4개의 스레드로 구성되어 있지만, 이를 UV_THREADPOOL_SIZE 환경 변수를 사용하여 최대 128개까지 늘릴 수 있습니다. 이 값을 조정하면 Node.js가 동시에 더 많은 작업을 처리할 수 있지만, 너무 크게 설정하면 오히려 성능이 저하될 수 있습니다.

스레드 풀 크기 설정
이 명령은 스레드 풀 크기를 8개로 설정하고 app.js를 실행합니다. 이렇게 설정하면 8개의 스레드가 동시 작업을 처리할 수 있게 됩니다.

UV_THREADPOOL_SIZE=8 node app.js

Thread Pool의 크기 확인 예제

아래 코드는 Node.js에서 Thread Pool의 크기를 확인하고, 스레드 풀 크기를 4개로 설정했을 때와 6개로 늘렸을 때 각각의 처리 시간을 비교하는 예제입니다.

const crypto = require('crypto');
const start = Date.now();

function encryptTask(id) {
  crypto.pbkdf2('password', 'salt', 100000, 64, 'sha512', () => {
    console.log(`Task ${id} 완료:`, Date.now() - start, 'ms');
  });
}

// 4개의 작업을 실행 (기본 Thread Pool 크기인 4에 맞춰)
for (let i = 1; i <= 4; i++) {
  encryptTask(i);
}

// 추가로 작업을 요청해 스레드가 어떻게 처리되는지 확인
encryptTask(5);
encryptTask(6);
  • 기본적으로 Thread Pool의 크기는 4개입니다. 따라서 처음 4개의 작업(Task 1 ~ Task 4)은 동시에 처리됩니다. 이후에 실행된 Task 5Task 6은 대기하게 되며, 첫 번째 그룹의 작업이 끝난 후에 처리됩니다. 이로 인해 추가 작업들이 처리 시간이 더 길어지는 현상이 발생합니다.
  • 처음 4개의 작업은 동시에 실행되며 약 130~140ms에 완료됩니다.
  • 추가 작업(Task 5, Task 6)은 이전 작업이 완료된 후, 즉 스레드 풀이 작업을 처리할 수 있는 상태가 되자 처리됩니다. 이때 두 번째 그룹의 작업은 첫 번째 그룹보다 시간이 더 걸립니다.

스레드 풀의 사이즈를 6개로 늘린 뒤 위 코드를 실행해보겠습니다.

UV_THREADPOOL_SIZE=6 node app.js
  • 모든 작업은 동시에 실행되며 약 130~140ms에 완료됩니다.
  • 스레드 풀의 크기를 늘렸기 때문에 6개의 작업이 모두 동시에 실행됩니다. 작업들이 대기하지 않고 처리되기 때문에 모든 작업이 비슷한 시간에 완료됩니다.


Thread Pool을 사용하는 작업 예시

fs.readFile 메서드는 Thread Pool을 사용하여 파일을 읽습니다. 메인 스레드(이벤트 루프)는 이 작업을 기다리지 않고 다른 작업을 처리할 수 있습니다. 파일을 다 읽으면 스레드 풀에서 결과를 이벤트 루프로 전달하고, 콜백 함수가 호출됩니다.

const fs = require('fs');

// 파일을 비동기적으로 읽음 (Thread Pool 사용)
fs.readFile('largeFile.txt', (err, data) => {
  if (err) {
    return console.error(err);
  }
  console.log('파일 내용:', data.toString());
});

Thread Pool과 Event Loop의 차이점

  • Event Loop는 자바스크립트 코드, I/O 이벤트 등을 처리하는 싱글 스레드의 논리적 흐름입니다. 이벤트 루프는 작업이 비동기적으로 완료되기를 기다리면서, 그 사이 다른 작업을 처리함으로써 시스템의 효율을 극대화합니다.
  • Thread Pool은 비동기 작업(특히 CPU 집약적인 작업이나 파일 시스템 작업)을 병렬로 처리하기 위한 멀티 스레드 환경입니다. 여러 개의 스레드를 한 번에 생성해두고, 요청이 들어올 때 이 스레드들을 활용해 작업을 동시에 처리하는 방식입니다. 이를 통해 스레드 생성 비용을 절약하고, 자원 사용을 효율적으로 관리할 수 있습니다. 주로 CPU 바운드 작업(계산 중심의 작업)이나 I/O 바운드 작업(파일 읽기/쓰기, 네트워크 요청 등)에 활용됩니다.

Worker Threads 모듈

Worker Threads 모듈을 사용하면 직접적으로 스레드를 생성하여 CPU 집약적인 작업을 처리할 수 있습니다. 이는 기본적으로 Thread Pool과 비슷한 개념이지만, 더 복잡한 작업을 효율적으로 처리할 수 있도록 고안되었습니다.

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

if (isMainThread) {
  // 메인 스레드에서 Worker 생성
  const worker = new Worker(__filename); // 현재 파일을 Worker로 사용

  worker.on('message', (count) => {
    console.log(`Worker 완료: ${count}`);
  });

  worker.on('error', (err) => {
    console.error('Worker 에러:', err);
  });

  worker.on('exit', (code) => {
    if (code !== 0) {
      console.error(`Worker가 종료되었습니다. 종료 코드: ${code}`);
    }
  });
} else {
  // Worker 스레드에서 실행될 코드
  let count = 0;
  for (let i = 0; i < 10000000; i++) count++;
  
  // 결과를 메인 스레드로 전송
  parentPort.postMessage(count);
}
profile
함께 개선하는 프론트엔드 개발자

0개의 댓글