ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JS] 태스크 큐와 이벤트 루프
    programing/Language 2019. 7. 31. 18:33

    안녕하세요, Einere입니다.

    (ADblock을 꺼주시면 감사하겠습니다.)


    이번 포스트에서는 태스크 큐와 이벤트 루프에 대해 알아보도록 하겠습니다.

    해당 포스트는 toast meetup의 김동우님의 글을 요약 및 정리한 글입니다.

     

     

    자바스크립트와 이벤트 루프

    1. ECMAScript와 이벤트 루프

    ECMAScript 에는 동시성이나 비동기와 관련된 언급이 없다 (ES6부터 살짝 변화가 있다)

    V8과 같은 자바스크립트 엔진은 단일 호출 스택(Call Stack)을 사용하며, 요청이 들어올 때마다 해당 요청을 순차적으로 호출 스택에 담아 처리할 뿐이다.
    이 자바스크립트 엔진을 구동하는 환경, 즉 브라우저나 Node.js가 비동기와 동시성을 담당한다.

     


    위와 같이, setTimeout이나 XMLHttpRequest같은 함수는 자바스크립트 엔진이 아닌 Web API에 정의되어 있다.

     


    Node.js는 비동기 IO를 지원하기 위해 libuv 라이브러리를 사용하며, 이 libuv가 이벤트 루프를 제공한다.
    자바스크립트 엔진은 비동기 작업을 위해 Node.js의 API를 호출하며, 이때 넘겨진 콜백은 libuv의 이벤트 루프를 통해 스케쥴되고 실행된다.

    자바스크립트가 '단일 스레드' 기반의 언어라는 말은 '자바스크립트 엔진이 단일 호출 스택을 사용한다'는 관점에서만 사실이다.
    실제 자바스크립트가 구동되는 환경(브라우저, Node.js등)에서는 주로 여러 개의 스레드가 사용되며, 이러한 구동 환경이 단일 호출 스택을 사용하는 자바 스크립트 엔진과 상호 연동하기 위해 사용하는 장치가 바로 '이벤트 루프'인 것이다.

     

     

    2. 단일 호출 스택과 Run-to-Completion

    Run-to-completion scheduling is a scheduling model in which each task runs until it either finishes, or explicitly yields control back to the scheduler.

    Run-to-completion이란 말은, 각 태스크는 다른 태스크가 완료된 후에 실행되거나, 명시적으로 스케쥴러에게 컨트롤을 반환하는 스케쥴링 모델을 의미한다.
    운영체제에서 흔히 말하는 비선점 스케쥴링이라고 할 수 있다.
    앞서 말했듯이, 자바스크립트 엔진은 하나의 호출 스택을 사용하며, 현재 스택에 쌓여있는 모든 함수들이 실행을 마치고 스택에서 제거되기 전까지는 다른 어떠한 함수도 실행될 수 없다.

     

     

    3. 태스크 큐와 이벤트 루프

    태스크 큐는 콜백 함수들이 대기하는 큐(FIFO) 형태의 배열이며, 이벤트 루프는 호출 스택이 비워질 때마다 큐에서 콜백 함수를 꺼내와서 실행하는 역할을 한다.
    MDN의 이벤트 루프 설명을 보면 왜 '루프'라는 이름이 붙었는지를 아주 간단한 가상코드로 설명하고 있다.

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

    위 코드의 waitForMessage()메소드는 현재 실행중인 태스크가 없을 때 다음 태스크가 큐에 추가될 때까지 대기하는 역할을 한다.
    이런 식으로 이벤트 루프는 '현재 실행중인 태스크가 없는지'와 '태스크 큐에 태스크가 있는지'를 반복적으로 확인하는 것이다.
    간단하게 정리하면 다음과 같을 것이다.

    • 모든 비동기 API들은 작업이 완료되면 콜백 함수를 태스크 큐에 추가한다.
    • 이벤트 루프는 '현재 실행중인 태스크가 없을 때'(주로 호출 스택이 비워졌을 때) 태스크 큐의 첫 번째 태스크를 꺼내와 실행한다.

     

     

    4. 비동기 API와 try-catch

    $('.btn').click(function() { // (A)
        try {
            $.getJSON('/api/members', function (res) { // (B)
                // 에러 발생 코드
            });
        } catch (e) {
            console.log('Error : ' + e.message);
        }
    });

    비동기 방식의 API들은 이벤트 루프를 통해 콜백 함수를 실행한다.
    위의 코드에서 버튼이 클릭되어 콜백 A가 실행될 때 $.getJSON 함수는 브라우저의 XMLHttpRequest API를 통해 서버로 비동기 요청을 보낸 후에 바로 실행을 마치고 호출 스택에서 제거된다.
    이후에 서버에서 응답을 받은 브라우저는 콜백 B를 태스크 큐에 추가 하고 B는 이벤트 루프에 의해 실행되어 호출 스택에 추가된다.
    하지만 이때 A는 이미 호출 스택에서 비워진 상태이기 때문에 호출 스택에는 B만 존재할 뿐이다.
    즉 B는 A가 실행될 때와는 전혀 다른 독립적인 컨텍스트에서 실행이 되며, 그렇기 A 내부의 try-catch 문에 영향을 받지 않는다.

    (마찬가지 이유로 에러가 발생했을 때 브라우저의 개발자 도구에서 호출 스택을 들여다봐도 B만 덩그라니 놓여있는 것을 볼 수 있을 것이다.)
    (이런 이유로 Node.js의 비동기 API들은 중첩된 콜백 호출에 대한 에러 처리를 위해 '첫 번째 인수는 에러 콜백 함수' 라는 컨벤션을 따르고 있다)

    $('.btn').click(function() { // (A)
        $.getJSON('/api/members', function (res) { // (B)
            try {
                // 에러 발생 코드
            } catch (e) {
                console.log('Error : ' + e.message);
            }
        });
    });

    이를 해결하기 위해서는, 위와 같이 콜백 B의 내부에서 try-catch를 실행해야 한다. (물론, 이렇게 해도 네트워크 에러나 서버 에러는 잡을 수 없다. 이를 위해서는 에러 콜백을 따로 제공해야 한다.)

     

     

    5. setTimeout(f, 0)

    setTimeout(function() {
        console.log('A');
    }, 0);
    console.log('B');

    태스크 큐와 이벤트 루프를 이해한다면, 위 코드의 의도를 알 수 있다.
    위 코드는 A를 출력하는 함수를 태스크 큐에 추가한다. 그리고 B를 출력한 뒤, 호출 스택이 비게 되어, 이벤트 루프가 태스크 큐에 저장된 함수를 호출 스택에 넣어준다.
    따라서, 출력 순서는 B -> A가 된다.

    한가지 짚고 넘어갈 사실은 0 이라는 숫자가 실제로 즉시를 의미하지 않는다는 점이다.
    브라우저는 내부적으로 타이머의 최소단위(Tick)를 정하여 관리하기 때문에, 실제로는 그 최소단위만큼 지난 후에 태스크 큐에 추가되게 된다.
    그리고 이 최소단위는 브라우저별로 조금씩 다른데, 예를 들어 크롬 브라우저의 경우 최소단위로 4ms 사용하기 때문에 크롬에서 setTimeout(fn, 0)은 setTimeout(fn, 4)와 동일한 의미를 갖게 된다.

     

     

     

    프로미스와 이벤트 루프

    이런 이벤트 루프의 개념은 실제로 HTML 스펙에 정의되어 있다. 문서에서 이벤트 루프, 태스크 큐의 개념에 대해 잘 정의되어 있는 것을 볼 수 있을 것이다.
    그런데 문서 중간에 마이크로 태스크(microtask) 라는 생소한 용어가 보인다.

    setTimeout(function() { // (A)
        console.log('A');
    }, 0);
    Promise.resolve().then(function() { // (B)
        console.log('B');
    }).then(function() { // (C)
        console.log('C');
    });

    위 코드는 B -> C -> A순서로 실행된다. 왜냐하면 프로미스는 마이크로 태스크를 사용하기 때문이다.
    마이크로 태스크는 쉽게 말해 일반 태스크보다 더 높은 우선순위를 갖는 태스크라고 할 수 있다.
    즉, 태스크 큐에 대기중인 태스크가 있더라도 마이크로 태스크가 먼저 실행된다.
    위의 예제를 통해 좀더 자세히 알아보자.
    setTimeout() 함수는 콜백 A를 태스크 큐에 추가하고, 프라미스의 then() 메소드는 콜백 B를 태스크 큐가 아닌 별도의 마이크로 태스크 큐에 추가한다.
    위의 코드의 실행이 끝나면 태스크 이벤트 루프는 (일반)태스크 큐 대신 마이크로 태스크 큐가 비었는지 먼저 확인하고, 큐에 있는 콜백 B를 실행한다.
    콜백 B가 실행되고 나면 두번째 then() 메소드가 콜백 C를 마이크로 태스크 큐에 추가한다. 이벤트 루프는 다시 마이크로 태스크를 확인하고, 큐에 있는 콜백 C를 실행한다.
    이후에 마이크로 태스크 큐가 비었음을 확인한 다음 (일반) 태스크 큐에서 콜백 A를 꺼내와 실행한다.

     

    그럼 또다른 예제를 보자.

    const promise_1st = new Promise((res, rej) => {
        console.log('1st');
        res();
    });
    
    console.log('2nd');
    
    setTimeout(() => console.log('6st'), 0);
    
    const promise_2nd = promise_1st.then((val) => {
        console.log('4st');
    });
    
    setTimeout(() => console.log('7th'));
    
    const promise_3rd = promise_2nd.then((val) => {
        console.log('5th');
    });
    
    console.log('3rd');
    
    /* 출력 결과 (크롬 콘솔)
    1st
    2nd
    3rd
    4st
    5th
    undefined // 호출 스택에서 전역 컨텍스트가 제거됨
    6st
    7th
    */

    스펙에 따르면 위 순서가 맞지만, 브라우저 내부 구현에 따라 timer가 promise보다 먼저 스케줄링 되는 경우도 있다고 합니다.

    제가 위 결과로 알아낸 사실은, promise 객체 생성시 주어지는 콜백 함수는 동기적으로 실행이 되지만, 값의 평가는 나중에 then이 실행될 때 이루어진다는 것입니다. 이는, 1st가 제일 먼저 찍히는 것을 보면 알 수 있습니다.

     

     

    로우 레벨로 살펴보는 Node.js의 이벤트 루프

    https://evan-moon.github.io/2019/08/01/nodejs-event-loop-workflow/index.html?fbclid=IwAR0p6runAxuIfSqrQm785mH12Zc_OORJ1e8KDtSqbtEp1lnr772LmEEM4cY

     

    로우 레벨로 살펴보는 Node.js 이벤트 루프

    1년 전, 필자는 setImmediate & process.nextTick의 차이점에 대해 설명하면서 Node.js의 이벤트 루프 구조에 대해 살짝 언급한 적이 있었다. 놀랍게도 독자 분들은 원래 설명하려고 했던 부분보다 이벤트 루프 부분에 대해서 더 많이 관심을 주었고, 필자는 그 부분에 대해서 많은 질문을 받았었다. 그래서 이번에는 Node.js

    evan-moon.github.io

     

    출처

    https://meetup.toast.com/posts/89
    https://en.wikipedia.org/wiki/Run_to_completion_scheduling

     

    자바스크립트와 이벤트 루프 : TOAST Meetup

    자바스크립트와 이벤트 루프

    meetup.toast.com

     

    Run to completion scheduling - Wikipedia

     

    en.wikipedia.org

    http://sculove.github.io/blog/2018/01/18/javascriptflow/

     

    자바스크립트 비동기 처리 과정과 RxJS Scheduler

    오늘 소개할 부분은 작성 중인 책의 “부록” 중 일부이다.이 장에서는 RxJS Scheduler를 잘 사용하기 위해서 이해해야할 자바스크립트 비동기 처리 과정을 살펴본다. RxJS를 모르는 사람이라도 자바스크립트의 비동기 처리과정을 이해하면 자바스크립트를 개발하는데 정말 많은 도움을 준다.더 자세한 내용은 다음 URL을 참고하여 꼭! 읽어보기 바란다 참고

    sculove.github.io

     

    댓글

Designed by black7375.