Event-Loop Part 1 : Big Picture

Event-Loop Part 1 : Big Picture
from : https://unsplash.com/photos/l9AtwCpsTuU

사전지식

Process & Thread

  1. 내가 생각하는 Process & Thread
  2. process와 thread에 대한 정리
  3. Process Management (Processes and Threads)

I/O Device

  1. Basics of OS (I/O Structure)
  2. I/O Systems in Operating Systems | Device Drivers | interrupt Polling

심플한 결론

  1. 내 index.js를 읽고 실행한다
  2. 내 코드를 읽으면서 파일 읽기/쓰기 네트워크 통신 관련 작업은 OS나 libuv에게 맡긴다(일이 끝나면, 내가 설정해 놓은 callback function이 실행되거나 event-queue에 넣어진다)
  3. 이벤트 루프를 돌리면서, event-queue에 있는 callback function을 하나하나 빼내서 실행한다

설명

I/O Device가 일을 마칠때 까지 기다릴 필요가 없다

내 서버에 client가 브라우저로 index.html파일을 달라고 요청했을때, 서버는 HardDisk에게 index.html파일을 달라고 다시 요청한다. 이때 만약 서버의 CPU가 HardDisk로부터 응답을 기다리기만하고 다른일을 안하면 너무 비효율 적일 것이다.

그래서 일단 일을 시키고, CPU는 다시 자기 일 한다음에, HardDisk가 일이 끝났을때 통보하도록 설정해 놓는게 효율적이다.

(실제로는 polling이라는 방식을 통해서 OS가 해당 I/O Device가 일을 다 마쳣는지를 계속 점검한다)

I/O작업을 마치고 나서 해야할 일을 정해줘야 한다.

개발자는 서버(서버 프로그램)가 HardDisk로부터 index.html파일을 받은 후에 뭘할지를 정해줘야 한다. NodeJS에서는 이런 I/O작업을 마치고 나서 그 결과를 가지고 시킬일을 callback function이라고 부른다. 개발자는 I/O 작업이 끝난다음에 NodeJS가 실행 해야할 callback function을 설정해 줘야한다.(주로 HardDisk에서 읽어들인 index.html을 client에게 다시 보내주는 일을 callback function으로 설정할 것이다)

요청은 일단 다 받고, callback function은 하나씩 처리

서버는 수많은 client로부터 “index.html파일을 보내줘!” 라는 요청을 받는다. 이렇게 I/O요청이 정말 많이 일어나기 때문에, 처리해야할 callback function도 많아진다. NodeJS에서는 event queue와 event loop를 사용해서 많은 callback function들을 처리한다.

서버는 I/O요청을 받아서 HardDisk나, Network Device에게 일을 시키고 그 일이 끝나면 event queue 에 (개발자가) 설정해 놓은 callback function을 순서대로 담고, event loop는 그 callback function을 하나하나 빼서 실행한다.

Event Queue는 하나가 아니다

네트워크 관련 I/O 작업들은 주로 OS를 통해서 OS의 프로세스 자원을 사용하여 처리되며, 파일 입출력 및 암호만들기 등은 libuv라는 멀티 플랫폼 비동기 I/O처리 라이브러리의 Thread Pool안에있는 thread에서 처리를 해준다. 타이머는 이벤트 루프안에서 정해진 시간이 지났는지를 계속 체크한다(따로 thread나 OS의 기능을 사용해서 돌리는것 같지는 않다).

이렇게 일의 종류가 다양한 만큼, callback function이 들어갈 event queue도 여러개이다.

(1) timers : setTimeout(), setInterval()로 등록한 callback 함수와 호출 시간이 들어있다. event loop는 timers queue에 들어있는 타이머 아이템(callback 함수 , 호출시간)에 설정된 호출시간이 지났는지 판단해서 callback function을 호출한다.

(2) I/O events : I/O 작업이 끝난후에 호출될 callback function이 들어있다.

(3) Immediates : setImmediate()로 설정된 callback function이 들어있다.

(4) close handler : *.on(‘close’)로 설정된 callback function이 들어있다.

Event Loop Phases

event loop는 libuv의 uv_run()에서 시작된다. 그리고 그 안에서 특별한 역할을 담당하는 함수들이 아래 이미지와 같은 순서대로 호출된다. 각각의 함수 혹은 단계를 phase라고 부른다.

(1) timers : timer event queue를 한바퀴 돌면서 지연 시간이 지난 타이머들의 callback function을 호출한다.

(2) pending callbacks : pending queue에 있는 callback function들을 처리한다. 아마도 I/O Operation의 결과값들(ex. network I/O)혹은 그것의 주소값을 같이 갖고있을것 같다. 그래야 callback function호출할때 인자값에 넣을 수 있기 때문이다. poll단계에서 처리되지 못한(다음 루프로 지연시킨) callback function들이 이곳에 담겨있을 것이다.

(3) idle : uv_idle_{init,start,stop} API를 사용해여idle queue에 등록된 callback function을 호출한다. (용도를 모르겠다)

(4) prepare : uv_prepare_{init,start,stop} API를 사용하여 prepare queue에 등록된 callback function을 호출한다. poll phase에서 loop가 잠깐 block될 수 있기 때문에, block되기 전에 실행할것들이 여기에서 실행된다.

(5) poll : 여유가 있다면 I/O Operation이 끝나기를 기다린다. 이렇게 기다리다가 어떤 I/O Operation이 끝나면 pending queue에 넣지 않고 여기서 바로 실행한다. 예를들어, 다음 타이머가 실행되기 까지 10초가 남았고 다른 큐에는 할일이 없다면, 10초동안 I/O Operation(파일 읽기, 쓰기)이 끝나기를 기다린다.

(6) check : setImmediate()로 설정된 callback function들을 호출한다.

(7) close callbacks : *.on(‘close’)로 설정된 callback function들을 호출한다.

(8) 위의 그림에는 나와있지 않지만, 각 phase사이사이에서 next tick queue와 micro task queue에 들어있는 callback function들을 호출한다.

실제 코드는 여기서 확인할 수 있다.

Multi Thread

nodejs는 single-thread이다. 하지만, 이것은 event-loop와 내 javascript코드를 실행할때만 맞는말이고, 실제로는 아래와 같은 경우에는 다른 thread를 추가적으로 사용한다.

(1) 암호만들기(crypto 모듈) : CPU를 많이 사용해서 Event Loop가 Block될 수 있기 때문에 thread를 사용한다

(2) 비동기 file I/O : Network I/O와는 다르게 OS가 비동기적으로 지원하지 않기때문에 thread를 사용한다

둘다 너무 오래 걸리거나 언제 끝날지 모르기 때문에, event loop가 block되는걸 막기 위해서 thread를 사용하는것이다.

nodejs의 비동기 I/O 추상화 라이브러리인 Libuv는 기본적으로 4개의 thread를 가지고 있다(추가 가능). 내가 fs 모듈의 readFile()함수나, crypto 모듈의 pbkdf2()함수를 사용하면 내부적으로 이런 작업들을 thread를 사용해서 처리한다. thread도 이벤트 루프처럼 안에서 work queue를 감시하는 loop가 돌고있고,

  1. 내가 index.js 에서 fs.readFile()을 사용하면
  2. 저 work queue에 내 요청이 담기고
  3. thread가 그것을 빼내서(원래 loop를 돌면서 work queue를 감시하고 있었으니까) 실행하고 OS에게 일을 맡긴다
  4. OS가 일 끝났다고 OS프로세스의 버퍼에서 읽어들인 파일 Data를 가져가라고 하면
  5. thread는 watcher queue에 들어있는 내 read요청에 파읽 읽기를 완료했다고 적어놓는다.
  6. 위 그림의 poll단계에서는 이 watcher queue를 감시하면서 변화(준비-> 완료)가 있는 요청의 callback function을 호출한다.

(사실 위처럼 심플하지는 않고, open -> stat -> read -> close 이런 과정을 거쳐서 마지막에 내 callback function이 호출된다)

Multi Thread에 관한 추가적인 내용은 [NodeJS] nodejs는 single-thread가 아니다 에서 확인 가능하며, readFile에 관한 내용은 [NodeJS] fs.readFile() 추적기(작성중..) 에서 확인 가능하다.

Big Picture

위에서 설명한것들을 모아서 그림을 다시 그려보면, 아래와 같다.

그림에는 나와있지 않지만, node index.js를 터미널에서 치면 내 index.js파일을 먼저 실행하고, libuv의 uv_run()을 실행한다. 그 안에서 여러 phase를 거친다. setTimeout() 이나 setInterval()로 설정된 callback function이 들어있는 timer phase, I/O Operation callback function이 들어있는 pending queue를 돌리는 pending phase, I/O Operation상태를 감시하여 I/O 관련 callback을 바로 호출하거나 다음 루프로 지연시키는 poll phase, setImmediate로 설정한 callback function이 담겨있는 queue를 돌리는 check phase, *.on(‘close’)로 설정한 callback function이 담겨있는 close handler queue를 돌리는, close phase를 마지막으로 거친다. 또한 각 phase 사이사이마다 next tick queue & micro task queue를 돌린다. Event loop를 block시킬 수 있는 작업들은 thread Pool의 thread에서 시키고, Network I/O작업은 OS에게 맡긴다. 또 이 work thread와 event loop가 돌아가는 main thread는 worker queue를 통해서 소통한다(pipe라는 기능을 쓴다는데 정확히는 잘 모르겠다).

마지막으로

지적과 질문은 환영입니다. 읽어주셔서감사합니다.

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다

Up Next:

Node.js는 single-thread가 아니다

Node.js는 single-thread가 아니다