ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Javascript] Eventloop(이벤트루프)
    Front-end/Javascript 2020. 5. 5. 12:01
    반응형

    대부분 김동우님, thms200님의 글을 통해 이해의 목적으로 작성하였습니다.

    Javascript와 단일스레드

     Javascript는 타 프로그래밍 언어와 다르게 '단일 스레드' 기반이다. 즉, 동시에 하나의 작업만 처리할 수 있다는 의미인데, 사용자들이 보는 웹은 전혀 그래보이지 않는다. Javascript 다른 무언가가 있는걸까?

     운영체제(Operating Systems)를 공부해보면 프로세스 스케쥴링(CPU 프로세스를 효과적으로 사용하기 위한 스케쥴링)에서도 이번 포스트의 예와 비슷하게 비유할 수도 있을 것 같다. 프로세스들을 스케쥴링을 통해 아주 잘 처리하면 동시에 처리하는 듯한 느낌이 들기도 한다.

     다단계 큐에 기반한 스케쥴링은 우선 순위에 따라서, 처리해야할 태스크(task)들을 큐(Queue)에 넣는다. 그림1을 보면 직관적으로 Level1이 가장 높은 우선순위를 갖고 있을 것 같다. 요약하자면, Level1의 태스크들이 다 마무리 되어야만 Level2에서 진행 할 수 있다. 하지만 처리하던 중 Level1에 처리해야할 태스크가 생기면 CPU를 다시 반납해야한다.

    그림1. 다단계 큐(Multi-level Queue) 출처: www.brittlethings.com

     

     Javascript도 작동방식이 완벽히 같지는 않지만, 동시성(Concurrency)를 지원하기 위해 이 스케쥴링 방법에서 아이디어를 얻은 듯 하다. 본격적으로 Javascript의 방식에 대해서 알아보도록 하겠다.

     


    Event Loop

     실제 작동하는 방식을 MDN에서 참고하려 하였으나, 아래의 영문과 코드에서 볼 수 있듯이 그다지 친절한 설명은 아니다. 하지만 한가지 챙겨가야 할 개념은 있다. 자바스크립트의 함수가 실행되는 방식을 보통 "Run to Completion" 이라고 말한다. 이는 하나의 함수가 실행되면 이 함수의 실행이 끝날 때까지는 다른 어떤 작업도 중간에 끼어들지 못한다는 의미이다.

     

    The event loop got its name because of how it's usually implemented, which usually resembles. queue.waitForMessage() waits synchronously for a message to arrive (if one is not already available and waiting to be handled)

    while (queue.waitForMessage()) {
      queue.processNextMessage()
    }

     

     

     자 그렇다면 잘 이해하기 위해서 다양한 자료를 모으는 노력이 필요하다.

    그림2. 브라우저 작동방식

     V8과 같은 자바스크립트 엔진은 단일 호출 스택(Call Stack)을 사용하며, 매 요청마다 순차적으로 호출 스택에 담아 처리할 뿐이다. 우리가 스크립트 상에서 쓰는 비동기 요청과 동시성은 어떻게 처리되는 것일까? 그 답은, 바로 이 자바스크립트 엔진을 구동하는 환경, 즉 브라우저나 Node.js가 담당한다. 즉, 브라우저에서 콜백 큐(Callback Queue)와 이벤트 루프(Event Loop)를 통해서 자바스크립 엔진에서의 단일호출스택을 관리한다. 

     Call Stack: 자바스크립트 엔진에서 저장되는 함수(콜백 제외)

                       Stack을 따라, LIFO(Last In First Out, 후입선출)을 따른다.

     Callback Queue: 비동기적으로 실행될 콜백 함수가 보관되는 영역 (setTimeout,...)

                               Queue의 이름에서 오듯 FIFO(First In First out, 선입선출)을 따른다.

     Event Loop: 멀티큐로 예를 들었듯, 콜 스택(Call Stack, level 1)이 빈 상태가 되면, 콜백 큐(Callback Queue, level2)에서 꺼내온 첫번째 콜백을 실행시킨다.

     

    Callback stack, Callback Queue, Event Loop의 개념을 머리에 담고서, 아래의 예시를 통해서 보다 쉽게 이해하도록 해보자.

     

    function delay() {
        for (var i = 0; i < 100000; i++);
    }
    function foo() {
        delay();
        bar();
        console.log('foo!'); // (3)
    }
    function bar() {
        delay();
        console.log('bar!'); // (2)
    }
    function baz() {
        console.log('baz!'); // (4)
    }
    
    setTimeout(baz, 10); // (1)
    foo();

     

     직관적으로는  setTimout(baz, 10)이 호출스택에 먼저 쌓이고, 10ms후에 끝난 후에야 foo()를 실행하면 baz -> bar -> foo 순으로 콘솔에 표시되어야할 것 같다. 하지만, 실제로는 그렇지않다.

     아래의 그림3처럼 setTimout은 콜백 함수이기 때문에, baz를 anonymout를 Callback Queue 남기고, 타이머 이벤트를 요청한 후 사라진다. 그 후,  foo, bar이 실행 된 후에서야 Callback Stack이 빈 것을 확인한 브라우저는 Callback Queue의 baz를 실행할 것이다.

    그림3. 코드 작동 순서

     


    Microtask Queue vs Callback Queue

     Event Loop에 Callback Queue이외의 Microtask Quque라는 한 녀석이 더 나타났다.

    console.log('script start'); 
    
    setTimeout(function() {
      console.log('setTimeout');
    }, 0);
    
    Promise.resolve().then(function() {
      console.log('promise1');
    }).then(function() {
      console.log('promise2');
    });
    
    console.log('script end');

     

     위의 코드의 결과를 Case1과 같이 생각 할 것이다. 하지만, Event Loop의 우선 순위는 Call Stack > Microtask Queue > Callback Queue이기 때문에, Case2와 같은 결과가 나온다. 즉 우리가 비동기적으로 흔하게 쓰는 Promise는 Microtask에 속한다는 결론이 된다.

     

    Case1
    script start
    promise1
    promise2
    script end
    setTimeout
    
    Case2
    script start
    script end
    promise1
    promise2
    setTimeout

     

    Event Loop가 우선순위를 두는 항목들을 보면 그림1을 수정해보자면 Level1(CallStack)부터 Level3(Callback Queue)까지처럼 나열이 되는 것이다.

    그림4. 이벤트루프 우선순위


    참고

    https://meetup.toast.com/posts/89

    https://www.brittlethings.com 

    https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop

     

    반응형

    댓글

Designed by Tistory.