[ 백엔드 공부하기 : Node.js ] NodeJS의 스레드 방식은 싱글? 멀티?
∇백엔드_NodeJS : Node.JS의 스레드 방식은 싱글인가? 멀티인가?
결론적으로 말하자면,
Node.js는 싱글로 작동하는 듯 하지만 확실하게 싱글이 아니고, 멀티로 작동하는 듯 하지만 확실하게 멀티는 아닙니다.
[ 이론적으로는 멀티 스레드인듯 보이지만, 개념적으로는 싱글 스레드 ]
이게 뭔 개소리냐..할 수 있지만,
이렇게 말할 수 있는게 { 'libuv'의 'thread pool' } 과 {Worket_thread} 의 작동 때문입니다.
Ⅰ . 프로세스와 스레드 ( Process & Thread )
▣ 프로세스란?
- 프로그램이 컴퓨터에서 실행이 되고 있는 상태로 만들어주는 실행 프로그램.
- 메모리에 올라와 실행되고 있는 프로그램의 인스턴스 (독립적인 객체) 입니다.
=> 운영체제로부터 자원을 할당받은 작업의 단위.
=>> 최소 1개 이상의 스레드를 가집니다.
▷ 프로그램이 실행되는 즉시 CPU로부터 할당받는 자원 영역 ( 메로리 등)
▣ 스레드란?
- 프로세스 내에서 실제로 작업을 처리하는 주체입니다.
- 프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위입니다.
- 스레드끼리 프로세스의 자원을 공유하면서 프로세스 실행 흐름의 일부가 됩니다.
▷ 프로세스 내에서 실행되는 여러 흐름의 단위이며,
프로세스가 할당받은 시스템자원 ( CPU 시간, 메모리 여역 등)을 이용하는 실행 단위.
▷ 스레드는 프로세스의 어떤 자원을 활용하게 될까?
:: 프로세스의 메모리 영역인 -> { Code, Data, BSS, Heap, Stack }에 접근하여 활용하게 됩니다.
▣ 멀티 스레드란?
- 하나의 프로세스가 두 개 이상의 스레드를 가지는 경우이며, 여러 작업을 여러 스레드를 사용하여
동시에 처리하는 것을 의미합니다.
- 모든 스레드는 프로세스 내 메모리 영역의 내용을 공유합니다.
- 메모리 자원을 아낄 수 있으며, 응답 시간이 빠르다는 장점이 있지만,
동기화 문제(계속 변동되는 자원에 접근하게 되는 오류)가 발생할 수 있습니다.
▣ 프로세스의 메모리.
:: 프로세스의 메모리는 크게 [ Code, Data, BSS, Heap, Stack ] 영역으로
√ Code.
- 가장 하단에 위치한 영역이며, 작성한 기계어 또는 코드가 들어갑니다.
- 함수, 제어문, 상수 등이 들어갑니다.
- Read-only 영역이며, 컴파일러가 만든 코드라고 봐도 무방합니다.
√ Data.
- 전역변수, 정적변수, 배열, 구조체 등이 저장되는 공간입니다.
- 초기화 (initialized) 된 데이터가 저장되는 공간입니다.
즉, 초기값이 있는 static 변수가 들어갑니다.
√ BSS (Block-started Symbol).
- 전역변수, 정적변수, 배열, 구조체 등이 저장되는 공간입니다.
- 초기화되지 않은 (초기값이 없는) 데이터가 저장됩니다.
📌 Code, Data, BSS 모두 정적인 영역이며, compile time에 크기가 결정되어
이후에 변동되지 않습니다.
📌 또한, 프로세스가 종료될 때까지 계속 작동합니다.
√ Heap.
- 프로그래머가 동적으로 사용하는 영역으로,
동적 객체 데이터의 할당 또는 반환이 이루어지는 영역입니다.
√ Stack.
- 함수가 포함되어 있고, 함수 내의 지역변수, 매개변수 등이 저장되어 있는,
프로그램이 자동으로 사용하는 임시 메모리입니다.
- LIFO(선입후출) 정책을 사용하며, 함수 호출 시 생성되고 함수 종료 시 반환됩니다.
📌 Heap의 영역이 증가하여, Stack 영역을 침범하는 Heap Overflow 의 상황이 되거나
Stack의 영역이 증가하여, Heap의 영역을 침범하는 Stack Overflow의 상황이 될 때 사용되는
메모리의 자유 영역 또한 존재합니다.
📌 프로세스 메모리의 속도는 Stack -> Data -> Code -> Heap 순으로 빠릅니다.
▣ Single, Multo-thread
▼ Single Thread :
- 하나의 프로세스에서 하나의 스레드를 실행하므로, 프로세스 내의 작업을 순차적으로 실행합니다.
- 유저와 상호작용하는 앱의 경우, 한 가지 작업이 끝난 뒤에야 다음 작업으로 이동하기 때문에
유저가 원하는 작업 수행이 빠르게 이루어지지 않을 수 있습니다.
(low-responsiveness)
▼ Multi Thread :
- 하나의 프로세스 내에 여러 개의 스레드가 실행됩니다.
- 각각의 스레드가 다른 작업을 할당받으므로, 프로세스가 병렬적으로 여러 작업을 동시에 수행
- Multi-Thread는 프로세스 내에서 각각 Stack만 따로 할당받고 Code, Data, Heap 영역은 공유합니다.
- Heap 메모리는 서로 동시에 읽고 쓸 수 있으며, 한 스레드가 자원을 변경하면 다른 이웃 스레드도
그 결과를 즉시 볼 수 있습니다.
- 여러개의 스레드가 동일한 메모리 주소를 활용하면서 작업을 여러 개 수행하기 때문에,
메모리 사용이 효율적입니다.
Ⅱ . JavaScript는 Single-thread인가?
자바스크립트를 작동시키는 것은 바로 V8엔진이며, V8엔진은 Call-Stack과 Memory Heap으로 구성되어 있습니다.
이 V8엔진 덕분에 자바스크립트는 브라우저 내*외부 모두 동작이 가능해졌습니다.
V8엔진과 런타임이 만나면 이벤트 루프가 작동하게 됩니다.
-> 이벤트 루프는 비동기 처리를 가능하게 해주는 일종의 시스템.
◎ "이벤트 루프"는 싱글 스레드로 작동합니다. [ 이것이 자바스크립트가 싱글 스레드 언어라고 불리는 이유 ]
== 자바스크립트 자체만으로는 코드가 순차적으로 실행됩니다.
[이전 라인 코드 실행이 안료되지 않으면, 다음 라인으로 넘어가지 않습니다]
📌자바스크립트는 Node나 웹 브라우저와 같은 멀티 쓰레딩이 가능한 환경에서 실행됩니다.
:: 따라서, "자바스크립트(엔진)" 은 싱글 스레드이지만, !
"자바스크립트를 실행하는 런타임"은 완벽하게 싱글 스레드 환경을 제공하지는 않습니다.
Ⅲ . NodeJs가 Multi-thread로 작동하는 것처럼 하는 libuv
Node.js 는 실행 즉시 하나의 독립된 프로세스가 되어 CPU로부터 리소스를 할당받습니다.
Node에서 자바스크립트를 실행하면 '콜백'을 통해 여러 개의 비동기 함수를 실행할 수 있으며,
fs.readfile 또는 DNS read 과 같은 작업들도 수행 가능합니다.
그러나!
이러한 작업들의 문제점은, 이벤트 루프에 큰 부담을 주어 결과적으로 애플리케이션의 성능이 줄어들게 됩니다.
이를 해결하기 위해서는 Node.js는 libuv 라는 C/C++ 라이브러리를 사용하기 시작합니다.
libuv는 멀티스레딩을 지원해줍니다.
◆ libuv
- I/O-intensive 작업과 CPU-intensive 작업을 지원합니다.
- I/O: 입력(Input)/출력(Output)의 약자로,
컴퓨터 및 주변장치에 대하여 데이터를 전송하는 프로그램, 운영 혹은 장치입니다.
- I/O-intensive : DNS, File System 관련 내용입니다.
- I/O-intensive : Crypto, Zlib 관련 내용입니다.
◇ Node.js가 libuv를 사용하여 새로운 스레드를 사용하는 구체적인 상황은???
1. DNS 쿼리를 할 때
- Host의 ip를 읽어야 하는 상황에서는 메인-스레드인 이벤트 루프를 사용하지 않고
새로운 스레드를 만들어서 작업을 수행합니다.
- Fetch API 등을 활용 때 등등.
2. File system에 접근할 때
- Async (비동기) file system 작업을 할 때,
이벤트 루프에서 자체적으로 새로운 스레드로 해당 작업을 넘깁니다.
- 주의할 점은, readfilesync 와 같은 동기 함수는 새로운 스레드가 아닌 이벤트 루프에서 실행.
3. Crypto, Zlip
- Crypto를 활용한 암호화나 해싱 작업 도한 이벤트 루프가 아닌 스레드를 활용합니다.
- Zlip : 코드 내에서 무언가를 압출할 때 역시 스레드를 활용합니다.
4. Cpu 집약적인 작업
- 암호화 작업과 같은 계산이 많이 필요한 작업을 처리.
5. 커널에서 지원하지 않는 비동기 작업
- 운영체제가 기본적으로 비동기 인터페이스를 제공하지 않은 작업을 처리.
6. 파일 시스템 작업.
- 일부 파일 I/O 작업을 처리합니다.
★ 결론적으로, NodeJS는 두 가지의 스레드로 구성되어 있습니다.
1. 이벤트 루프 (메인 스레드)
2. Thread Pool :
- 미리 생성된, 비어있는 스레드 그룹입니다.
- Thread Pool의 크기를 미리 정해놓으면
반복적인 스레드 생성/삭제 절차를 줄임으로써 프로세스의 성능을 개선 가능합니다.
[ Node.js의 Thread Pool 기본값은 4개 입니다. ]
** Nodejs는 이벤트 루프에서 작업을 수행하다가, fs 나 해싱등의 작업이 필요한 경우,
이 작업들을 Thread Pool로 넘겨서 새로운 Thread를 사용합니다.
📌 중요한 POINT.
- res.end 등과 같이, networking 관련 작업은, 항상! 메인 스레드인 이벤트 루프를 활용합니다.
- 서버의 코드를 작성할 때 해당 코드의 작업이 이벤트 루프에서 작동하는지,
쓰레드풀의 다른 스레드에서 이루어지는지 판단하는 것은 매우 중요합니다.
[ 무조건 스레드를 늘려놓는 것 보다, 어떤 코드가 새로운 스레드를 요구하는지 파악하고,
이에 맞춰서 pooling 작업을 지속적으로 하는 것이 중요합니다. ]
✨ 정리하면, Nodejs는 기본적으로 Js의 이벤트 루프를 메인 스레드로 활용하기 때문에
싱글 스레드이지만, 기본 작업 외 특정 작업들을 수행할 때 추가 스레드가 필요한 경우네는
쓰레드 풀에서 새로운 스레드를 생성하여 실행할 수 있는
(유사) Multi-thread 프로세스이기도 합니다.
Ⅳ . NodeJs가 Multi-thread로 작동하는 것처럼 하는 모듈 :: Worker_Threads 모듈
▣ "워커 스레드" 는 CPU 퍼포먼스 향상을 위해 활용되는 모듈입니다.
▣ Worker Thread 정리.
Worker_thread는 Node.js에서 JavaScript 코드를 병렬로 실행할 수 있게 해주는 기능입니다.
주요 특징은 다음과 같습니다
-
- 독립적인 실행: 각 Worker_thread는 독립적인 V8 인스턴스와 이벤트 루프를 가집니다
- 메시지 기반 통신: 스레드 간 통신은 메시지를 통해 이루어집니다. postMessage() 메소드로 메시지를 보내고, 'message' 이벤트 핸들러로 받을 수 있습니다
- 리소스 공유: 메모리를 공유하지 않고 메시지만 주고받아 안전성을 높입니다
- CPU 집약적 작업에 적합: I/O 작업보다는 CPU를 많이 사용하는 작업에 효과적입니다
▣ Worker Thread 사용.
- const { worker, parentPort } = require('worker_threads')
-> worker 클래스는 독립적인 자바스크립트 실행 스레드를 의미하고, parentPort는 메세지 포트의 인스턴스입니다. - new Worker(filename) 이나 new Worker(code, {eval: true})
-> 워커를 시작하는 두 가지 메인 방법입니다(파일명을 넘기거나, 실행하고자 하는 코드를 작성하거나).
실제 제작시 파일명을 사용하는 편이 권장됩니다. - worker.on('message'), worker.postMessage(data)
-> 다른 스레드간 메세지를 주고받을 때 사용합니다. - parentPort.on('message'), parentPort.postMessage(data)
-> parentPort.postMessage(data) 를 통해 보내진 메세지는 worker.on('message') 를 사용한 부모 스레드에서
사용 가능합니다. 그리고 worker.postMessage(data) 를 사용한 부모 스레드로부터 보내진 메세지는
parentPort.on('message') 를 사용한 스레드에서 사용 가능합니다.
메인 스레드 와 Worker 스레드
- isMainThread : 현재 코드가 메인 스레드에서 실행되는지, 워커 스레드에서 실행되는지 구분
- 메인 스레드에서는 new Worker를 통해 현재 파일(__filename)을 워커 스레드에서 실행시킴
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) { // 메인 스레드
const worker = new Worker(__filename); // 같은 dir폴더에 워커를 생성
} else { // 워커스레드
// 위에서 생성한 worker는 여기서 동작
}
메인 스레드 <-> Worker 데이터 송수신
- worker.postMessage로 부모에서 워커로 데이터를 보냄
- parentPort.on('message')로 부모로부터 데이터를 받고, postMessage로 데이터를 보냄
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) { // 메인 스레드
const worker = new Worker(__filename);
worker.on('message', (value) => {
console.log('워커로부터', value)
})
worker.on('exit', (value) => { // parentPort.close()가 일어나면 이벤트 발생
console.log('워커 끝~');
})
worker.postMessage('ping'); // 워커스레드에게 메세지를 보낸다.
} else { // 워커스레드
parentPort.on('message', (value) => {
console.log("부모로부터", value);
parentPort.postMessage('pong');
parentPort.close(); // 워커스레드 종료라고 메인스레드에 알려줘야 exit이벤트 발생
})
}
▣ Worker Thread 작동 원리.
워커 스레드는 Node.js의 메인 스레드와 별도로 동작하게 됩니다.
개발자는 워커 스레드를 생성하여
복잡한 계산이나 CPU 집약적인 작업을 메인 스레드와는 독립적으로 수행시킬 수 있습니다.
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('message', message => console.log(message));
worker.postMessage('Hello, Worker!');
} else {
parentPort.on('message', (message) => {
parentPort.postMessage(message + ' Received');
});
}
이 코드는 메인 스레드에서 워커 스레드를 생성하고, 워커 스레드와 메시지를 주고받는 간단한 예제입니다.
워커 스레드는 메시지를 받고, 처리한 후 메인 스레드에 결과를 전송합니다.
왜냐하면 워커 스레드는 메인 스레드와 독립적으로 실행되기 때문에,
메인 스레드의 실행을 방해하지 않고 복잡한 작업을 처리할 수 있습니다.
이는 Node.js 애플리케이션의 성능과 반응성을 향상시키는 데 큰 도움이 됩니다.
워커 작업의 핵심은 무언가 하드한 코드가 있으면,
이를 개발자가 이 코드를 잘 쪼개서 나뉘어 할당하는 로직을 구현하여 나누는 능력이 필요하고,
워커에서 돌리고 나온 결과나 설정 등등 하나하나 개발자가 세세하게 직접 코딩해야 한다는 특징이 있습니다.
Ⅴ . 그럼 libuv 와 Worket_Thread 의 차이점과 공통점은 뭐야?
◎ 차이점.
1. 목적
● libuv thread pool : 주로 I/O 작업과 일부 CPU 집약적인 작업을 처리합니다.
● worker_thread : CPU 집약적인 JS 작업을 수행하는 데 유용합니다.
2. 제어권.
● libuv thread pool : Node.js 내부에서 자동으로 관리되며, 개발자가 직접 제어하지 않습니다,
● worker_thread : 개발자가 명시적으로 생성하고 관리합니다.
3. 사용범위.
● libuv thread pool : 파일 시스템 작업, DNS 조회, 암호화 작업 등에 사용됩니다.
● worker_thread : JS 코드를 실행할 수 있어 더 다양한 작업을 처리할 수 있습니다.
4. 통신 방식.
● libuv thread pool : Node.js 내부에서 자동으로 처리됩니다.
● worker_thread : 메시지 패싱을 통해 통신합니다.
- 'postMessage()' 와 'message' 이벤트를 사용합니다.
◎ 공통점.
1. 멀티 스레딩 지원
● 둘 다 Node.js에서 멀티스레팅을 가능하게 합니다.
2. 성능 향상
● 둘 다 Node.js 애플리케이션의 성능을 향상시키는 데 사용될 수 있습니다.
3. 병렬 처리
● 둘 다 작업을 병렬로 처리할 수 있게 해줍니다.
4. 메인 스레드 블로킹 방지
● 둘 다 메인 이벤트 루프가 블로킹되는 것을 방지하는 데 도움을 줍니다.
'Back_End > Node.js' 카테고리의 다른 글
[ 백엔드 공부하기 : Node.js ] NodeJS의 비동기(Async) 흐름 제어 및 이벤트 루프. (0) | 2024.12.10 |
---|---|
[ 백엔드 공부하기 : Node.js ] NodeJS의 NPM이란? (0) | 2024.12.07 |
[ 백엔드 공부하기 : Node.js ] NodeJS의 기본 동작 원리와 이벤트 루프, 브라우저 환경을 벗어난 JS 실행. (1) | 2024.12.06 |
[ 백엔드 공부하기 : Node.js ] NodeJS에 활용되는 JavaScript의 기본 동작 원리와 V8엔진. (2) | 2024.12.05 |
[ 백엔드 공부하기 : Node.js ] Node.js는 서버단에서 어떻게 JS를 실행할까. (0) | 2024.11.24 |