[JS] Javascript는 싱글 스레드(Single Threaded)인데 어떻게 병렬처리(다중처리)가 가능할까?

[JS] Javascript는 싱글 스레드(Single Threaded)인데 어떻게 병렬처리(다중처리)가 가능할까?

#목차

JavaScript의 Runtime환경

Thread 란?

Thread는 브라우저가 사용자 이벤트를 처리하고 디스플레이를 렌더링하여 화면에 그려주고, 일반적인 웹 페이지 또는 앱을 구성하는 코드의 대부분을 실행하는 데 사용한다.” - MDN

js-engine

Memory Heap(변수,객체 저장소): 객체와 변수를 할당하는 데 사용되는 비구조화 메모리의 한 부분.
Call Stack(코드 실행): 실행 될 코드들이 메모리(Heap)에 할당된 변수 또는 객체를 찾아 호출되면서 코드가 실행되고 실행된 코드들은 Stack에서 제거.

  • 즉, 위의 그림처럼 Javascript Engine(Single Threaded)에는 Memory HeapCall Stack으로 각 1개씩 구성되어 있다.

Memory Heap 이란?

“JavaScript는 객체가 생성될 때 자동으로 메모리를 할당하고 더 이상 사용되지 않을 때 메모리에서 제거한다(garbage collection).
이 자동성은 잠재적인 혼란의 원인된다. 왜냐하면, 개발자에게 메모리 관리에 대해 걱정할 필요가 없다는 잘못된 인상을 줄 수 있다.” - MDN

Memory life cycle

  1. 필요한 메모리 할당
  2. 할당된 메모리 사용(읽기, 쓰기)
  3. 할당된 메모리가 더 이상 필요하지 않을 때 해제(제거)

JavaScript에서의 할당

  • 값 초기화.
  • 자바스크립트는 초기에 값이 선언될 때 자동으로 Memory Heap에 할당한다.
var a = 10;                             // stack에 저장
let b = 'hello';                        // stack에 저장
const varPI = Math.PI;                  // stack에 저장
const singleThreadExample = () => {     // stack에 저장
  return b + " " + (varPI * a);
}

Call Stack 이란?

Call Stack은 인터프리터(웹 브라우저의 자바스크립트 인터프리터)가 여러 함수를 호출하는 스크립트에서 자신의 위치를 추적하는 메커니즘.” - MDN

  • 스크립트가 함수를 호출하면 인터프리터는 함수를 호출 스택에 추가한 다음 함수를 수행하기 시작.
  • Heap영역에 있는 해당 함수에 의해 호출되는 모든 함수는 스택에 추가되고 호출이 된 곳에서 기능을 실행.
  • 현재 기능이 완료되면 인터프리터는 스택에서 해당 기능을 제거하고 마지막 코드 목록에서 종료한 위치에서 실행을 다시 시작.
const multiply = (x, y) => {    
    return x * y;
}
function printSquare(x) {   
    let s = multiply(x, x);
    console.log(s);
}
printSquare(5); // Call Stack 영역

// 25

call stack flow

Stack Overflow

  • 스택이 할당된 공간보다 더 많은 공간을 차지하면 “stack overflow” 오류가 발생합니다.
const foo = () => {
  foo();
}
foo();

stack overflow

WEB APIs 란?

js-webapis

“웹 API는 자바스크립트 엔진의 일부는 아니지만 브라우저가 제공하는 런타임 환경의 일부이다. 현대의 브라우저에는 매우 다양한 것을 가능하게 하는 수많은 API가 있다.”

  • Manipulate documents(문서 조작): 가장 일반적으로 사용되는 웹 API 중 하나는 개발자가 HTML과 CSS를 조작할 수 있게 해주는 DOM API로 HTML을 사용하면 HTML을 생성, 변경 및 제거하고 웹 페이지에 스타일을 동적으로 적용할 수 있다.
  • Draw and manipulate graphics(그래픽 그리기 및 조작): 브라우저에서 널리 지원되는 Canvas API 및 Web Graphics Library API를 사용하면 <canvas> 요소에 포함된 픽셀 데이터를 프로그래밍 방식으로 업데이트할 수 있다.
  • Fetch data from a server(서버에서 데이터 가져오기): Fetch API는 Request 및 Response 개체의 일반적인 정의를 사용하여 네트워크를 통해 리소스를 가져오는 인터페이스를 제공.

이벤트 수신기, 타이밍 함수 및 AJAX 요청과 같은 기능은 모두 작업이 트리거될 때까지 웹 API 컨테이너에 저장한다. 요청이 데이터 수신을 완료하거나 타이머가 설정된 시간에 도달하거나 클릭이 발생하면 콜백 함수가 콜백 대기열로 전송한다. - WEB APIs

Tasks(Callback) Queue 란?

js-queue

Tasks(Callback) Queue는 웹 API에서 전송된 콜백 함수를 추가된 순서대로 저장한다. 이 대기열은 선입선출로 실행되는 데이터 구조이다. 큐는 배열 푸시 방식을 사용하여 큐의 끝에 새로운 콜백 함수를 추가하고 배열 이동 방식을 사용하여 큐의 첫 번째 항목을 제거한다.”

콜백 함수는 콜 스택이 비워질 때까지 대기열에 저장된 다음 이벤트 루프에 의해 스택으로 이동이 된다.

Event Loop 란?

Event Loop의 역할은 콜 스택과 콜백(Tasks Queue) 대기열의 상태를 지속적으로 모니터링하는 것이다. 스택이 비어 있으면 콜백 큐에서 콜백을 가져와 콜 스택에 배치하여 실행을 예약한다.”

결론!

js_runtime

  • 이것이 자바스크립트가 단일 스레드 언어임에도 불구하고 비동기적으로 실행될 수 있는 것으로 묘사되는 이유이다.
  • 자바스크립트는 한 번에 하나의 기능만 실행할 수 있기 때문에 동기화되지만 웹 API에서 콜백 큐로 콜백을 푸시 할 수 있고 이벤트 루프가 이러한 콜백을 지속적으로 콜 스택에 추가할 수 있기 때문에 자바스크립트는 비동기적으로 실행될 수 있다고 생각한다.

작동원리

  1. JS Engine이 Heap영역에 변수를 저장해 놓는다.
  2. JS Engine이 Stack영역에 작성한 코드들을 넣고 실행한다.
  3. setTimeout, Ajax, async, Event Listener과 같은 비동기 작업들은 WEB APIs에 작업들을 넘긴다.
  4. WEB APIs에서의 작업들은 Queue영역에와서 Stack영역이 비어있을때 Event LoopStack영역으로 작업을 올려준다.

이해를 돕기위한 그림(Call Stack, WEB APIs)

js-callstack 그림출처

Call Stack


js_WEB_APIs 그림출처

WEB APIs

추가 자료

콜 스택(Call Stack)과 힙(Heap)

  • 콜 스택(Call Stack) : 원시타입 값과 함수 호출의 실행 컨텍스트(Execution Context)를 저장하는 곳.
    • 원시타입이란? : 객체가 아니면서 메서드도 가지지 않는 데이터 타입(string, number, bigint, boolean, undefined, symbol, null)
    • 실행 컨텍스트(Execution Context)란? : 실행할 코드에 제공할 환경 정보들을 모아놓은 객체
      • 예제
var temp = 'temp';
function b () { console.log('안녕하세용'); }
function a () { b(); }
a();

Execution Context 실행 컨텍스트 환경

  • 힙(Heap) : 객체(Object), 배열(Array), 함수(function)와 같이 크키가 동적으로 변할 수 있는 참조타입 값을 저장하는 곳.

예제를 통한 동작 원리

let a = 10;
let b = 35;
let arr = [];
function func() {
  const c = a + b;
  const obj = { d: c };
  return obj;
}
let o = func();

1. GEC(Global Execution Context - 글로벌 실행 컨텍스트)가 생성되고 원시 값은 콜 스택에, 참조 값은 힙에 저장된다.


Memory1 GEC 컨텍스트 JS Single Thread

2. 함수 func() 을 실행하게 되고 새로운 FEC(Function Execution Context)가 생성되며 동일하게 원시 값은 스택에, 참조 값은 힙에 저장된다.


Memory2 FEC 컨텍스트 JS Single Thread

3. 이후, 함수 func() 이 객체 obj 를 리턴해서 o 에 저장된다. 리턴하기 때문에 FEC는 콜 스택에서 제거된다.


Memory3 작업을 마친 FEC 컨텍스트 JS Single Thread

4. 전체 코드가 실행이 끝나고 GEC가 콜 스택에서 제거된다. GEC가 제거됨에 따라서, 힙의 객체를 참조하는 스택의 값이 없기 때문에 가비지 컬렉터(Garbage Collector, GC)에 의해 제거된다.

추후 공부할 것

Reference