setTimeout & setImmediate 그리고 V8 이벤트 루프

00_8_3·2023년 2월 22일
0

실행 순서가 바뀌는 이유

이벤트 루프는 6개의 페이즈로 구성되어 있다.

  • setTimeout은 timer phase

  • setImmediate는 check phase

각 페이즈를 돌면서 실행 가능한 콜백을 발견하면 순차적으로 실행을 한다.

setTimeout(()=>{
	console.log("test1");
}, 0);

setImmediate(()=>{
  console.log("test2");
});

여기서 위 코드를 실행 해보면

출력 결과가

"test1"
"test2"

또는

"test2"
"test1"

이 반복되는 것을 볼 수 있다.
싱글 스레드는 순서를 보장해주는 것이 아닌가?

위 코드가 실행이 될 때
각 페이즈가 갖고 있는 큐에 들어 간다.

그리고 이벤트 루프가 각 큐를 방문 할 때 실행할 준비가 되면 실행이 된다.
그런데 맨 처음 실행되는 timer phase에 있는 setTimeout은 실질적으로는 1ms 후에 실행이 된다.

V8엔진, NodeJS에서는 최소 1ms 후 실행되게 설계되어 있음.

방문이 발생 할 때 시점이 1ms 전이냐 후이냐에 따라 실행 결과가 달라진 것이다.

방문이 1ms 전에 발생하는 경우 setTimeout은 준비가 되지 않았기 때문에
"test2"가 먼저 실행이 되고

만약 방문이 1ms 후에 발생하는 경우 setTimeout이 준비가 되었기 때문에 "test1"이 먼저 실행 되었던 것이다.

이러한 이벤트 루프의 방문 또는 큐에 진입속도는 cpu에 상태에 따라 조금 씩 달라질 수 있다.

\

6페이즈 외의 큐

마이크로테스크 큐

  • Promise가 resolve된 상태의 콜백 큐이다.

넥스트틱 큐

  • process.nextTick의 콜백 큐
  • 두개의 큐는 libuv에 포함되어 있지않다. (이벤트 루프가 아니다)
  • 넥스트틱 큐가 마이크로테스크 큐보다 우선순위가 높다.
  • 콜백이 준비가 되면 먼저 실행이 된다.
const fn = () => {
	process.nextTick(fn)
}

setTimeout(()=> {
	console.log("Never reachead");
}, 0)

fn()

위와 같은 형태의 코드인 경우 setTimeout는 영원히 실행하지 못한다.

  • 위의 동작은 Nodejs 11버전 이상 방식이다.

그 이전의 방식에서 브라우저의 방식과 통일하기 위해 현재의 형태로 바뀌었다.

net 패키지 'listening' 이벤트

net 패키지의 server.listen() 내부 구현 코드를 보면

nextTick을 사용하여 'listening' 이벤트를 emit 한다

setImmediate를 사용해도 될텐데 왜 그럴까?

setImmediate를 사용하였을 때

만약 'listening' 이벤트가 발생하기 전에 다른 이벤트가 먼저 check phase의 큐에 들어가게 된다면

'listening'의 콜백의 실행 시점이 약간 늦어 질 수 있어,

서버의 상태를 확인하려고 하는 코드가 정상 작동하지 않을 수 있다.

그래서 nextTick을 사용하여 check의 큐를 진입하기 전에 'listening'을 실행 시키는 것 이다.

조금 더 응용 (코드)

const net = require("net")

const server = net.createServer();

server.listen(8080, ()=>{
  console.log("서버 오픈")
});

setTimeout(()=>{
  console.log("setTimeout")
}, 0)

process.nextTick(()=>{
  console.log("before listen 1")
})

server.on('listening', () => {

  // listen 코드가 먼저 실행된 이유는
  // 콜백 함수가 nextTick으로 실행되기 때문이다.
  console.log("listen")
});

process.nextTick(()=>{
  console.log("before listen 2")
})

// 출력 순서
// 서버오픈
// listen              
// before listen 1
// before listen 2
// setTimeout

앞의 before listen 1 보다 listen이 먼저 출력되는 이유는
on의 등록은 동기적으로 되고
server.listen이 poll 페이즈에서 실행이 될 때 on의 콜백이 먼저 nextTick 큐에 진입했기 때문이다.

net : https://github.com/nodejs/node/blob/61b4d60c5d/lib/net.js#L1407

그 동안 막연하게 알고 있던 NodeJS의 이벤트 루프에 대해 다시 알아보았다.

지금 이 글을 작성하는 시점 이전에도 아래의 링크들을 참고해서 읽어 본적이 있었지만

이해를 하지 못하고 넘어 갔었다.

최근 회사에서 Go를 통한 작업을 하게 되면서

여러 자료구조들을 접하게 되었는데

그에 대한 영향인지 잘은 모르겠지만 갑자기 이해가 쏙쏙 되는 것 같은 느낌이다.

NodeJS 개발자로서 이벤트 루프의 내부 동작원리를 이해한다면

코드를 작성함에 있어 큰 도움이 있을 거라 생각이 든다.

이 글은 나만의 정리 방식이니 아래의 링크들을 꼭 참고 하였으면 한다.

페이즈의 모습

참고

0개의 댓글