CS를 개념적으로 이해하는 것도 중요하지만, 성능과 연결지어 생각해보고 논리적으로 말할 수 있어야 한다고 생각한다.
오늘은 스레드가 많으면 어떤 이점과 손실이 있는지 알아보자.
스레드란 프로세스 내에서 실행되는 작업 흐름의 단위다.
메모리위에 올라가 있는 프로세스를 효율적으로 처리하기 위해 스레드를 사용한다.
이 글의 제목이자 약간은 추상적인 질문인 '스레드가 많으면 무조건 좋을까?'에 대해 몇 가지 관점을 두고 풀어보고자 한다.
애플리케이션의 실행이 무수히 많은 작업들로 쪼개져 CPU 코어 수보다 몇십배는 많은 스레드가 생성되어 작업을 처리하면 어떨까?
몸이 좋아야 머리가 안아프다라는 말처럼..
이렇게 처리하면 스레드들은 순서에 상관없이 마구잡이로 작업(Task)을 처리할 것이다.
결국 애플리케이션의 데이터는 서로 동기화되지 않고
1번 뷰에서 가져온 데이터와 2번 뷰에서 가져온 데이터는 서로 다른 데이터가 될 확률이 높다.
즉, 스레드 세이프하지 않게 된다.
근데 만약 이 애플리케이션의 동작이 순차적으로 실행되어야만 하는 특징을 가진다면 어떻게 될까?
그래서 잘게 쪼개서 동시에 실행하기에 매우 어려운 성격의 애플리케이션이 있다면 실제로 사용할 수 있는 스레드 수는 한계가 있을 것이다.
여기서 스레드를 더 늘려봤자 순차실행에 필요한 개수 이상의 스레드를 사용하게 되면 별다른 이점이 없다.
CPU(혹은 코어)에서 실행되던 스레드는 멀티 태스킹 방식에 의해 짧은 시간만 동작하고 다른 스레드로 바뀐다.
이 때 코어는 기존 스레드의 정보 즉, 현재까지 진행한 스레드의 맥락을 저장해야 한다.
맥락을 저장한 뒤엔 새롭게 생성되는 스레드가 아니라면, 이전 정보를 가져와 이어서 작업을 진행한다.
이를 컨텍스트 스위칭이라고 한다.
프로세스나 스레드가 방금까지 실행했던 맥락(데이터, 컨텍스트)을 저장하는 주체는 운영체제다.
운영체제는 프로세스를 관리하는 프로세스 제어 블록(Process Control Block)을 갖고 있는데, 여기에 프로세스의 현재 상태(데이터)를 저장한다.
그리고 프로세스가 대기 혹은 준비상태로 변하며 다시 실행될 때 저장한 데이터를 불러온다.
근데 프로세스는 무겁다.
즉, 프로세스끼리는 공유하는 자원이 없기에 운영체제 입장에선 한 번에 많은 데이터를 PCB에 저장하고 다시 프로세스를 변경해야하기 때문에 무겁다.
그렇다면 스레드는 어떨까?
스레드는 독립적인 Stack 영역을 제외한 Code / Data / Heap 영역을 부모 프로세스에서 공유하고 있다.
따라서 운영체제 입장에선 스위칭이 일어날 때마다 모든 데이터를 저장할 필요가 없으므로, 컨텍스트 스위칭 비용이 줄어들게 된다.
이는 메모리/캐시와도 상관이 있는데, 오늘 주제에서는 벗어나니 다음에...
다시 컨텍스트 스위칭 관점에서 보자.
코어에 올라간 스레드가 다른 스레드로 변경되는 동안(컨텍스트 스위칭) 애플리케이션의 코드는 실행되지 않는다.
즉, CPU(혹은 코어)가 대기하는 CPU Time이 발생한다.
이 CPU Time은 애플리케이션과 직접적인 관련은 없지만 멀티 스레딩으로 동작하기 위해 필요하다.
이를 Overhead(간접 비용)이라 한다.
자, 스레드는 컨텍스트 비용이 적다.
왜? 공유하는 자원이 많으니까.
그래도 컨텍스트 스위칭을 하게 되면 CPU가 대기하는 시간이 발생한다.
이 시간을 오버헤드라고 한다.
스레드를 더 많이 사용하게 되면?
코어수는 고정되어 있고, 스레드 수를 계속 늘리게 되면 어느 순간에는 Overhead 때문에
성능면에서도 한계에 부딪칠 것이다.
갑자기 생소한 bound라는 용어가 나왔다.
우선 이 용어를 이해하기 전에 알아야 할 게 더 있다.
버스트.. 뭔가 게임에서 들어볼 법한 단어다.
어떤 현상이 짧은 시간안에 집중적으로 일어나는 것이다.
프로세스가 CPU에서 한 번에 '연속적'으로 실행되는 시간이다.
프로세스가 I/O 작업을 요청하고 그 '결과'를 기다리는 시간이다.
프로세스는 CPU burst와 IO burst의 연속이다.
즉, 처음에 메모리에 올라갈 땐 CPU burst
중간중간 CPU - IO를 반복하다가 프로세스 종료시엔 다시 CPU burst로 끝난다.
CPU burst가 많은 프로세스다.
유튜브에서 유튜버들은 각자 자신의 커뮤니티에 '영상 업로드' 시간이 오래 걸려서 업로드가 늦어졌다고 하는 경우가 빈번하게 있다.
또한 머신 러닝 프로그램의 경우도 뭔가 작업이 오래걸린다는 사실을 우리는 직접 경험해보지 않아도
몇 번씩 흘려들은 적이 분명히 있다.
(혹은 코인 채굴)
이러한 작업은 모두 CPU bound process다.
앞서 말했듯 프로세스가 한 번에 연속적으로 실행되는 시간이 길다.
연속적으로 실행되는 시간이 길다는 것은 처리해야 할 연산이 많다는 뜻이다.
따라서 CPU의 코어만으로 부족하기에 GPU를 사용해야 한다.
조금만 더 알아보자.
IO burst가 많은 프로세스다.
일반적으로 백엔드 API 서버를 예시로 작동 프로세스를 살펴보면
클라이언트로부터 HTTP Request를 받는다.
써드파티 DB 서버 혹은 캐시 서버에 Data를 요청한다.
받은 Data를 적당한 형태로 가공한다.
가공한 Data를 HTTP response로 내려준다.
여기서 DB 서버 / 캐시 서버에 요청하는 것이 API 서버에서의 IO 작업이다.
현재 실행중인 프로세스가 어떤 bound인지 확인하려면
귀를 사용하자.
만약 CPU 팬이 돌아가는 소리가 커진다면 그건 프로세서가 열일하고 있다는 뜻으로 CPU bound다.
그런데 하드디스크(SSD면 아무소리도 안날 것이다)에서 소리가 크게 난다면 IO bound다.
.....
좋은 방법이 있다면 따로 포스팅하겠슴...
여기서 주목해야할 건 멀티 코어, CPU bound이다.
만약 프로세스에 4개의 스레드가 있다고 생각하자.
코어가 2개이므로 각 코어에는 2개의 스레드가 매핑된다.
CPU bound 프로세스는 실행 시간이 길다.
하지만 멀티 태스킹 조건에 의해 각 코어 내부의 스레드들은 매우 빠른 속도로
컨텍스트 스위칭을 하게 된다.
일반적으로 CPU Time에는 컨텍스트 스위칭 시간도 포함되므로 불필요한 오버헤드가 발생한다.
그렇다면 스레드 수를 4개에서 2개로 줄인다면?
각 스레드는 1개의 코어에 매핑된다.
코어와 스레드가 1:1 매핑이 되어 스레드들간의 컨텍스트 스위칭이 발생하지 않게 되고
결과적으로는 불필요한 오버헤드가 발생하지 않게된다.
이 경우 IO 작업이 많아 CPU가 대기하는 시간이 많다.
따라서 코어 수보다 2~3배로 스레드를 늘려주는 것이 CPU의 오버헤드가 늘긴해도
코어들을 효율적으로 쓸 수 있기 때문에 성능면에서 이점이 크다.