1. Process vs Thread?

  1. The difference between Process & Thread(Operation System)

Process?

운영체제 위에서 독립적으로 실행되고 있는 프로그램
지금 위 사진처럼 운영체제 위에서 3개의 프로세스가 각각 메모리 위에서 서로 독립적으로 실행되고 있다. 만약 한 프로세스에 문제가 생기면 한 프로세스만 종료된다. 각각의 프로세스는 저마다 resorce(자원)들이 정해져 있다. 즉 프로세스마다 할당된 메모리나 데이터가 지정돼 있다.

프로세스 안에는?

Code(코드): 프로그램을 실행하기 위한 코드가 저장돼 있다.
Stack(순서): 프로세스 안에서 함수들이 어떤 순서로 실행돼야 하는지, 함수가 끝나면 어디로 다시 돌아가야 되는지에 대한 정보를 저장하고 있는 스택이 들어 있다.
Heap(동적데이터): 오브젝트를 생성하거나 데이터를 만들 때 그 데이터들이 저장되는 공간이다.
Data(전역/스태틱데이터): Heap에는 동적으로 할당된 변수들이 저장되는 반면, Data에는 Global 전역변수나 Static 변수들이 할당된다.

Thread?(일꾼)

한 프로세스 안에서 Thread는 여러개가 동작할 수 있다.
Thread는 각각 저마다 해야되는 업무를 배정받아 수행한다. Thread는 각자 본인이 수행해야 하는 함수의 호출을 기억해야 되기 때문에, Thread마다 stack(순서)이 할당되어져 있다. 하지만 이 Process 안에서 동작하는 Threads(일꾼)들은 결국은 한 프로그램을 위해서 일해야 되므로, Process에 지정된 Code, Heap, Data들을 공통적으로 접근해서 업데이트가 가능하다.

Multi-thread?

만약 내 프로그램에서 음악을 들으면서 사진을 편집할 수 있는 어플리케이션이 있다면, 각각 '음악을 재생하는 Thread' 하나와 '사진을 편집할 수 있는 Thread', 그리고 다른 Thread에서는 이 음악의 data를 서버에서부터 받아와서 처리하는 일을 맡는 등 이렇게 Thread는 각각 다른 일을 수행할 수 있다. 여러개의 Thread가 동시에 돌아가고 있는 것을 Multi-thread라고 하는데, Multi-thread는 동시다발적으로 발생할 수 있기 때문에 Process가 일을 더 효율적으로 처리할 수 있도록 도움을 준다.

Thread는 Process에 공통적으로 할당된 Resorce(Code, Heap, Data)를 동시다발적으로 업데이트 해가며, 공유해가며 사용한다.

Multi-thread Programming이 까다롭고 어려운 이유가 여기에 있다. Multi-threading을 잘못하면 공통적으로 업데이트하면서 순서가 맞지 않거나 하는 문제가 발생할 수 있기 때문이다.

Java: 언어 자체에서 multi-threading이 지원이 된다. 그 말은 우리가 프로그래밍을 짤 때, 사용자가 이 데이터를 보고 있는 동안 '서버에서 데이터를 받아오는 것은 Thread A에서 해야지', 그리고 '이 일은 Thread B에서 해야지'하고 각각 지정해서 프로그래밍을 짤 수 있다는 의미이다. 또한 총 몇 개의 Thread가 동시에 동작할 수 있게 할 것인지 등 굉장히 다양한 것을 구현할 수 있다. 그래서 Java로 Multi-threading을 구현하려면 배워야 하는 것이 많다.

JavaScript: 하지만 JavaScript는 single threaded language이다. 그 말은, 자바스크립트 언어 자체에는 multi-threading이 없다는 것이다. JavaScript 언어 자체에는 multi-threading을 할 수 있는 방법은 없지만, 이 JavaScript가 동작하고 있는 브라우저 위에는 여러가지 thread가 들어 있다. 그래서 우리가 브라우저 즉 웹 APIs들을 활용하면 multi-threading이 가능하다. 그리고 JavaScript가 동작하는 런타임 환경(실행 환경)에서는 event loop와 같은 다양한 방식을 이용해서 multi-threading 같은 효과를 얻을 수 있다.

JavaScript 엔진

우리의 웹 어플리케이션이 브라우저 위에 올라가는 순간 자바스크립트 엔진은 우리가 작성한 코드를 한 줄 한 줄씩 해석하고, 분석하고, 실행하게 된다. 이것은 정확히 어떻게 되는 걸까?

memory heap?

프로세스 안에 code, stack, heap, data가 있다고 배웠다. 이처럼 자바스크립트 엔진에도 memory heap과 call stack이 있다. 우리가 데이터를 만들면(변수 선언하여 오브젝트나 문자나 숫자열 할당) 그 데이터들은 모두 memory heap에 저장이 된다. memory heap은 구조적으로 정리된 자료구조가 아니라, 자료들이 여기저기에 아무렇게나 저장이 되어져 있다.

call stack?

함수를 실행하는 순서에 따라 차곡차곡 쌓아놓는 아이, 스택은 자료구조의 하나이다. stack은 LIFO(Last in First Out)의 자료구조를 가지고 있다. call stack에는 함수가 처음 실행되는 순서뿐만 아니라, 해당 함수가 다 실행되고 나면 어떤 함수로(어떤 위치로) 돌아가야 하는지의 순서까지 저장되어 있다. 그래서 아래 예시에서 second() 함수 실행이 끝나면 first()로 돌아가고, 그 다음 main()으로 돌아간다. main() 안에서 return을 실행하고 나면, 이제 콜스택에는 (더 이상 실행할 함수가 없으므로) 아무것도 남아있지 않고 비어있게 된다.

재귀함수는 지정된 call stack 사이즈를 초과하게끔 한다. 에러 발생!

function endless() {
endless();
}

endless();

자바스크립트 런타임 환경에서는 자바스크립트로 할 수 있는 것이 한정적이었지만, 브라우저에서 제공하는 웹 APIs를 사용하면 multi-threading을 이용해서 조금 더 다양한 일을 동시에 실행할 수 있다.

그 중 대표적으로 fetch를 이용해 백엔드에서 데이터를 받아온다던지, setTImeout을 이용해 일정 기간의 시간이 지난 다음에 우리가 등록한 콜백 함수를 실행하는 등 멋진 일들을 할 수 있다.

그렇다면 자바스크립트 엔진과 웹 APIs는 서로 어떤 순서로 일을 하게 되는 걸까? 이때 call stack(순서를 쌓아둔 자료구조)은 어떻게 되는 것일까? 자바스크립트 엔진과 웹 APIs가 어떤 식으로 대화를 하는지 알아보자!

Queue?

FIFO: FIrst In First Out

Queue의 대표적 예시, add와 remove
add(1)
add(2)
remove()를 하면 제일 처음에 들어왔던 숫자 1이 먼저 나가게 된다. (FIFO)

setTimeOut()으로 3초 후에 hello를 출력하는 콜백함수가 실행되도록 한다면 call stack은 어떻게 될까?
-call stack에 setTimeOut()이 저장됨
-setTimeOut()이 실행되는 순간, setTimeOut()은 콜백에서 지워지고 웹 API는 타이머를 시작한다.
-'자바스크립트 엔진'과 '웹 API가 실행하고 있는 타이머'는 병렬적으로 실행되고 있다가 지정된 시간이 끝나면
-웹 APIs는 Task Queue에 "야 타이머 끝났어 여기 네가 등록한 콜백이야"라며 이 콜백 함수 자체를 Task Queue(FIFO)에 집어넣는다.
*웹 APIs는 우리가 등록한 콜백함수를 원하는 때에 지정된 시간에 알아서 Task Queue에 넣어주는 것이다!
-Event Loop는 계속 빙글빙글 돌며 Call Stack과 Task Queue를 관찰하는 아이이다. 빙글빙글 돌다가 Call Stack에 뭔가 일이 남아 있으면, 이 Call Stack이 다 비워질 때까지 기다린다. 그래서 Call Stack이 텅텅 비어 자바스크립트 엔진이 더 이상 일을 하고 있지 않을 때, Task Queue에 있는 아이를 Call Stack으로 데리고 온다.
-그러면 자바스크립트 엔진이 Call Stack에 들어온 timeout이라는 콜백함수를 실행하게 된다.

event loop는 process가 동작하는 동안 계속 빙글빙글 루프를 돌면서 call stack이 비어져 있다면 task queue에 있는 아이를 call stack으로 가져와서 자바스크립트 엔진이 수행할 수 있도록 도와준다.

JavaScript 코드에 있는 button에 'click'에 대한 addEventListener를 등록해놓으면 어떻게 될까?
브라우저에서 버튼이 클릭되면 > 웹 APIs는 우리가 등록한 콜백함수를 task queue 안에 넣는다 > 버튼이 한 번 더 클릭되면 > 우리가 등록한 콜백함수가 한 번 더 task queue에 들어간다 > event loop는 call stack에 아직 timeout callback이라는 것이 수행이 되고 있기 때문에 (즉 call stack이 아직 비어 있지 않기 때문에) 기다렸다가 timeout callback이 다 수행이 되면 > event loop는 task queue에 있던 클릭의 콜백함수 하나를 call stack으로 가지고 온다(task queue에 있는 아이는 한 번에 하나씩만 call stack으로 가져올 수 있다) > click callback이 끝나면 > task queue에 있는 click callback을 다시 call stack으로 가져온다 > click callback 함수 실행이 끝나면 call stack은 다시 비워진다

자바스크립트 런타임 환경에는 (브라우저가 어떻게 구현되었느냐에 따라서) Task Queue 말고도 다양한 아이들이 있다.

웹 APIs의 런타임 환경

웹 APIs의 런타임 환경에는 총 3가지 아이들이 있는데,
Task Queue에는 우리가 흔하게 등록한 콜백함수들이 들어오게 되고,
Microtask Queue에는 Promise 안에 등록된 콜백들이 들어오게 되고,
Render에는 브라우저에서 우리가 변형한 코드가 주기적으로 업데이트 되기 위해서 주기적으로 호출되는 순서인데, Render에 저장되는 request animation frame의 공간에는 request animation frame이라는 API 안에 등록된 콜백이 큐에 차곡차곡 쌓인다.

Task Queue:

웹 APIs는 우리가 등록한 콜백 함수를 특정한 이벤트가 발생했을 때 task queue에 넣는다.

Microtask Queue:

아래 두 가지 함수의 콜백은 Microtask Queue라는 곳에 들어오게 된다.
-promise then: Promise가 다 수행이 되고 나면 그 다음에 호출되는 then에 등록된 콜백함수
-mutation observer

Render:

브라우저에 우리가 변형하는 DOM 요소가 표기되기 위해서는 render tree가 만들어져야 한다. 레이아웃의 크기와 위치들이 계산이 된 다음에 paint와 composite 과정을 통해 브라우저에 표기가 된다.

-Request Animation Frame: request animation frame("다음에 내 브라우저가 업데이트되기 전에 내 콜백을 실행해줘!")라는 API가 있는데, 이 API를 통해 콜백을 등록해 놓으면 그 콜백들은 이 request animation frame에 차곡차곡 쌓인다.
-Render Tree
-Layout
-Paint

콜백 안의 코드는 어떤 순서로 작성되어져 있던지 상관 없다. 자바스크립트는 콜백 함수 안의 코드 블럭이 다 완료될 때까지 기다렸다가 나중에 렌더링이 발생하기 때문이다.

(ex. 클릭 이벤트로 콜백함수를 등록했을 경우,
1) 클릭 이벤트가 발생하면
2) 웹 APIs는 우리가 등록한 콜백함수를 task queue에 보낸다.
3) 그러면 event loop가 빙글빙글 돌다가, task queue에 있는 아이템을 발견하고 call stack으로 가지고 온다. call stack은 이 콜백함수 실행이 완료될 때까지 기다렸다가(즉, 안에 있는 코드가 다 수행이 될 때까지 기다렸다가) call stack이 비게 되면 그때서야 event loop는 이 수정된 사항들을 다 적용해서
4) Render의 순서로 가게 된다.)

그래서 우리가 이 render tree를 만들 때 쯤에는, 콜백함수 안의 모든 코드들이 다 적용된 상태이기 때문에 콜백함수 안에서 코드를 어떤 순서로 작성하는지는 웹의 동작 속도가 아무런 관련이 없다.

event loop는 call stack에 뭔가 채워져 있으면, 그게 다 비워질 때까지 머물러 있으며 기다린다

(call stack에 등록되는) 콜백함수를 작성할 때, 코드 안에서 너무 많은 일이 이뤄지도록 작성하는 것은 좋지 않다. 왜냐하면 event loop가 콜백함수 안에 있는 코드가 모두 실행되어 call stack이 비워질 때까지 기다리는 동안, 브라우저는 업데이트가 되지 않아 사용자의 클릭 처리 등과 같은 다른 이벤트 처리도 발생하지 않기 때문이다. (따라서 만약 콜백함수 안에 무한대로 돌아가는 코드를 작성한다면..브라우저는 업데이트가 될 수 없어 아예 멈춰버리고 만다. 아래 예시에서 더 이상 버튼을 마우스로 후버했을 때 배경색이 바뀌지 않는다.)

따라서 콜백은 최대한 간단한 아이만 작성하는 것이 좋다.

while(true)

콜백함수 -> Call Stack (event loop은 call stack이다 비워질 때까지 기다리므로 브라우저의 event들은 정지됨)

btn

위 코드에서는 무슨 일이 일어날까?

0) 웹 APIs에서 버튼 리스너가 등록 되었고, 버튼에 이벤트가 발생하면 웹 APIs가 task queue에 콜백함수를 등록
1) event loop은 돌다가, task queue에 있는 콜백함수를 발견하고 이를 (1개만!) call stack으로 옮김
2) handleClick 실행, 안에 0초 뒤에 작동하는 setTimeout 함수를 발견
3) event loop은 0밀리세컨드를 기다리며 한바뀌 돌았다가 시간이 다 되면 setTimeout의 콜백함수인 handleClick()을 task queue에 넣음
4) event loop은 task queue에서 1개만 가져다가 call stack에 넣음 (handleClick()은 call stack으로 이동)
5) call stack에서 handleClick()이 수행
6) handleClick() 안에 있는 setTimeout이 호출됨
7) 0초 뒤에 setTimeout 안의 handleClick()이 task queue로 들어감

setTimeout > event loop 돌면서 event 처리하고 > call stack

event loop은 task queue에서 1개씩만 call stack으로 가져온다. setTimeout에 등록된 콜백은 기다리는 시간만큼 event loop이 돌면서 가끔 이벤트를 처리할 시간이 있기 때문에, 콜백이 재귀함수라고 해도 여전히 브라우저의 이벤트가 처리된다.

Promise > microtask queue > call stack > microtask queue > ... (event loop은 microtask queue가 비워질 때까지 브라우저를 정지하고 기다림)

요약

JavaScript 엔진에는 heap(동기적 데이타 저장)과 call stack(함수가 실행되는 과정을 기억하기 위해서 쓰이는 자료구조)이 있다.
JavaScript가 동작하는 런타임 환경에서는 task queue와 microtask queue를 이용해서 asynchronous(비동기적인) 처리를 하게 되는데, task queue는 한 번에 하나씩만 가지고 오고, microtask queue는 들어 있는 아이들을 모두 다 수행할 때까지 가지고 오게 되고, render 단계는 event loop가 주기적으로 (매번은 아니지만) 브라우저의 UI를 업데이트하기 위해 들러준다. request animation frame 안에 있는 콜백들은 브라우저의 업데이트가 일어나기 전에 (한꺼번에) 코드가 실행이 된다.

좋은 웹페이지 즐겨찾기