글의 목적
우리가 컴퓨터를 살때, CPU가 8코어 16스레드라고 하는 수치를 보고, 오... 16개의 스레드까지 병렬 프로그래밍이 가능하구나... 라는 생각을 하게 된다.
근데 운영체제에서 마주치는 "스레드"라는 단어는 위 내용과는 사뭇 다른 느낌으로 정의된 단어라는 느낌이 강렬하게 들게 된다.
또한, 스레드 풀에서의 스레드와도 비슷한 개념이면서 미묘하게 다른 점을 갖고 있고, 이게 혼동되기 쉽다고 생각한다.
따라서, 이를 체계적으로 정리하고자, 해당 글을 작성한다.
CPU 에 존재하는 스레드는 무엇인가?
해당 스레드라는 단어는, 원래 정식 명칭이 아니다.
과거에 이 "스레드"는, Simultaneous Multi-Threading 이라는 기법으로 불렸으나, 인텔(Intel) 의 상술로 "하이퍼스레딩" 이라는 상표명이 발표되면서, 해당 "스레드" 라는 단어를 사용하게 된 것이다.
사실상 여기서의 스레드는 "멀티스레딩이 가능한 최대 허용 가능한 스레드의 갯수" 로 보는게 바람직하다.
그럼 이제 진짜 "스레드" 에 대해 알아보자.
리소스
컴퓨터 프로그램을 작동시키기 위한 모든 기능과 기구의 총칭. 주기억 장치, 중앙 처리 장치, 입출력 장치, 데이터, 파일, 프로그램 등이 포함된다. 태스크가 실행되는 단계에서 제어 프로그램으로 각종 자원을 할당한다. - 정보통신용어사전(terms.tta.or.kr)
리소스는 기본적으로 작업 수행 과정 중, 할당되는 컴퓨터의 구성 요소 중 하나이다.
태스크
운영 체계의 모드 중 배치 모드와 다중 프로그래밍 모드에서 실행되는 프로그램을 구분하기 위해 도입된 개념으로, 다중 프로그래밍된 상태에서의 루틴 또는 컴퓨터의 수행 내용. 다중 프로그래밍 능력을 갖는 시스템에서 프로그램에 대한 순차 처리를 하기 위해 작업 단계라는 개념이 도입되었다. 따라서 작업은 둘 이상의 작업 단계로 구분되고 작업 단계 각각은 둘 이상의 태스크로 나뉜다. 태스크와 작업 단계의 또 다른 차이점은 전자의 경우 시스템 자원을 사용하기 위해서 동시에 경쟁이 가능하지만 후자의 경우는 순서적으로 시스템 자원을 사용할 수 있다. - 정보통신용어사전(terms.tta.or.kr)
태스크는 고유의 Control Flow를 가지며, 해당 제어 흐름을 따라 CPU의 자원을 활용하여 프로세스를 실행시킨다.
스레드란?
컴퓨터 프로그램 수행 시 프로세스 내부에 존재하는 수행 경로, 즉 일련의 실행 코드. 프로세스는 단순한 껍데기일 뿐, 실제 작업은 스레드가 담당한다. 프로세스 생성 시 하나의 주 스레드가 생성되어 대부분의 작업을 처리하고 주 스레드가 종료되면 프로세스도 종료된다. 하나의 운영 체계에서 여러 개의 프로세스가 동시에 실행되는 환경이 멀티태스킹이고, 하나의 프로세스 내에서 다수의 스레드가 동시에 수행되는 것이 멀티스레딩이다. - 정보통신용어사전(terms.tta.or.kr)
약간 덧붙이자면
- 멀티프로세싱
- 여러 CPU 코어를 사용하여 동시에 여러 작업을 수행하는 것
- 프로세스들은 독립적으로 실행되며, 각각 자신만의 주소 공간을 가짐
- 하드웨어 기반으로 성능을 향상
- 예시
- 다중 코어 프로세서를 사용하는 현대 컴퓨터 시스템
- 멀티태스킹
- 단일 CPU 코어가 여러 작업을 동시에 수행하는 것처럼 보이게 하는 것
- 소프트웨어 기반으로 CPU 시간을 시분할하여 각 작업에 할당
- 예시
- 현대 운영체제에서 여러 애플리케이션이 동시에 실행되는 환경
- 멀티쓰레딩
- 분류
- 동시성 멀티스레딩
- 여러 작업을 번갈아가며 실행하는 방식
- 단일 코어에서도 구현 가능
- 예: 시분할 시스템
- 병렬 멀티스레딩
- 여러 작업을 실제로 동시에 실행하는 방식
- 다중 코어 또는 다중 프로세서 환경에서 구현
- 예: SIMD(Single Instruction, Multiple Data) 연산
- 동시성 멀티스레딩
- 공통 특성
- 하나의 프로세스 내에서 여러개의 스레드를 동시에 실행
- 스레드들은 프로세스의 자원을 공유
- context switching 이 프로세스 레벨의 멀티태스킹보다 빠름
- 자원 공유로 효율적이지만, 동기화 문제에 주의해야 함
- 분류
- 출처:정보통신기술용어해설(ktword.co.kr)
라고 할 수 있다.
한마디로 스레드는, 일종의 제어 흐름을 가진 작업 단위이고, 해당 작업 단위를 "CPU" 라는 리소스를 이용해 적절하게 쪼개서 수행하는 것이, 현대 운영체제의 멀티스레딩 개념인 것이다.
왜 굳이 멀티 프로세스로 처리 가능한걸 멀티 스레드로 하는거지?
멀티 스레드를 사용하면, 프로세스를 생성하여 자원을 할당하는 시스템 콜이 감소함으로써 자원의 효율적 관리가 가능하다. 또한, 프로세스 간의 통신(IPC)보다, 스레드 간의 통신 비용이 적어 작업들 간의 부담이 감소하기 때문이기도 하다.
대신, 멀티 스레드를 사용할 때는 공유 자원으로 인한 문제 해결을 위해 '동기화'에 신경써야 한다.
컨텍스트 스위칭
CPU는 기본적으로, 캐시를 이용해 연산을 수행할때, 가장 빠른 속도를 보장한다.
이를 위해서
- 기존 작업을 "상위 저장소" 에서 "하위 저장소" 로 내려놓고,
- 신규 작업을 "하위 저장소" 에서 "상위 저장소" 로 데이터를 끌고 오는 작업
을 해야하는데, 이를 "컨텍스트 스위칭" 이라고 한다.
여기서 상위 저장소와 하위 저장소 관계는 - 캐시 - 메모리 관계
- 메모리 - 디스크 관계
- 디스크 - 외부 DB 관계
등 다양하게 적용 가능하다.
한마디로 Context Switching 은, CPU의 "연산 능력" 이 아닌, "기억 능력" 이 좌우하는 비용이라고 할 수 있다.
스레드가 많으면, 컨텍스트 스위칭 비용이 늘어나는 이유
스레드가 많다면? 메모리 사용량이 증가하게 된다.
- 각 스레드는 자체 스택과 레지스터 세트를 가진다.
- 스레드 수가 늘어나면 이러한 자원의 사용량도 증가한다.
그 여파로, - 캐시 효율성 저하
- 스레드 간 전환 시 캐시 내용이 변경되어 "캐시 미스"가 발생할 확률이 높아지게 된다.
- 메모리 접근 패턴이 복잡해짐
- page fault(메모리 - 디스크 간 "캐시 미스") 의 증가로 이어질 수 있다.
- 스케줄링 복잡성
- 운영체제의 스케줄러가 더 많은 스레드를 관리해야 하므로 스케줄링 결정이 복잡해진다.
- 선택지가 너무 많아지는 문제가 발생하는 것이다.
다시한번 말하지만, 컨텍스트 스위칭은 "기억 능력"이 좌우하는 CPU의 비용인 것이다.
CPU Bound 작업 vs I/O Bound 작업
이쯤에서, CPU에 의해 느려지는 작업과, I/O 에 의해 느려지는 작업들의 특징을 한번 비교해보고자 한다.
CPU Bound 작업
CPU Bound 작업은, 크게 두가지 요소에 의해 실행 속도가 결정된다.
- 코드의 최적화 정도
- 자료구조
- 알고리즘
- CPU의 연산 속도
예시로는 - 정렬과 같은 병렬화가 불가능한 복잡한 수학 연산
- 데이터 분석
- 비디오 인코딩
- 과학적 시뮬레이션
등이 있겠다.
위와 같은 작업들은 싱글 스레드로 동작하는 것이 효율적이다.
왜냐하면, CPU가 그야말로 "열심히" 일하고 있기 때문에, CPU가 놀고 있지 않다. 굳이 열심히 일하고 있는 놈을 끄집어내서 쫓아낼 이유는 없지 않은가?
참고) 병렬화가 가능한 CPU Bound 작업은 멀티 스레드로 동작하는 것이 효율적이다.
병렬화가 가능한 대표적인 경우는 각각의 연산 사이에 종속되는 부분이 없는, 독립적인 연산들이 많을 때를 들 수 있으며, 대표적으로 행렬 곱셈과 같은, 각 행렬 사이의 곱 연산이 다른 행렬 사이의 곱 연산에 영향을 미치지 않는 경우로 들 수 있다.
I/O Bound 작업
I/O Bound 작업은, 크게 두가지 요소에 의해 결정된다
- 컨텍스트 스위칭
- Hit Ratio
- 동시성 처리와 스케줄링 알고리즘
- Lock 관리
- 외부 장치와의 통신
- 디스크
- 네트워크
- 외부 DB
- 사용자
이런 상황에서는 멀티스레드로 동작하는 것이 효율적이다. 이 중, 개발자가 직접 제어 가능한 부분은 "외부 장치와의 통신" 이고, 따라서, CPU 의 자체 연산 속도보다, "외부 장치" 와의 통신 과정 사이에, CPU 를 다른 연산을 시키는 것이 효율적이기 때문이다.
이런 문제는 항상 "비용" 관점으로 생각하는 것이 옳다고 생각한다. CPU의 연산 속도는 "상한"이 정해져 있다. 하지만 메모리는, 돈을 쓰면 해결되는 부분이기에, 좀 더 경제적인 관점으로 접근하는 것이 옳기 때문이다.
따라서 이 과정에서, 좀 더 비싼 자원인 "CPU의 연산" 을 위해, "저장 장치" 를 희생시키는 것이 모든 동시성/병렬성 프로그래밍의 핵심이라고 보면 된다.
스레드 풀의 "스레드"
그렇다면 스레드 풀의 "스레드"는 무엇인가?
스레드 풀의 등장 개념
스레드는 운영체제 레벨에서 "작업의 흐름" 으로써 관리된다.
한마디로 리소스와 실행해야 하는 코드를 합친 하나의 "처리해야 하는 자원" 으로 추상화되어 동작한다는 뜻이다.
하지만 해당 "스레드" 객체를 생성하는 것은 상당한 오버헤드가 발생한다.
따라서, 해당 "스레드를 생성" 할때 오버헤드를 줄이기 위해, 미리 스레드를 생성해두는 것을 "스레드 풀링" 이라고 한다.
스레드 풀의 "스레드"란?
- 가상의 작업 흐름
- 빈 작업 흐름
- "CPU를 쉬게 하는" 작업이 기본으로 들어가 있음
- 유휴 상태를 유지하는 것이 해당 Control Flow의 목표인 것이다.
- 원래 스레드는 일종의 Control Flow이지만
- 프로그래머가 관리할 수 있는 "Resources" 형태로 추상화시켜놓은 것
- 동적으로 스레드 내 "control flow" 을 바꿀 수 있도록 하는 것
자바의 Executor 프레임워크의 스레드 풀에서 설정 가능한 스레드 관리 방법
- corePoolSize
- 스레드 풀에서 관리되는 기본 스레드의 수
- maximumPoolSize
- 스레드 풀에서 관리되는 최대 스레드의 수
- keepAliveTime, timeUnit
- 기본 스레드 수를 초과해서 만들어진 "초과 스레드" 가 생존할 수 잇는 대기 시간
- 이 시간동안 처리할 작업이 없다면 초과 스레드는 제거됨
- BlockingQueue workQueue
- 작업을 보관할 블로킹 큐
- RejectedExecutionHandler
- 스레드 풀의 "작업 거절 정책" 이다.
- 최대 스레드의 갯수를 초과했을 경우, 해당 작업을 처리하는 방법을 지정해줄 수 있다.
참고: 스레드 풀의 "초과 스레드(리소스)" 는 "기본 스레드(리소스)" + "블로킹 큐의 최대 길이" 보다 많은 스레드(태스크)가 들어왔을 때, 비상용으로 생성하는 "스레드(리소스)"로, 일반적으로 쉽게 상상할 수 있는 느낌으로 생성되는 스레드가 아니다.
여기서도 문맥에 따라 "스레드" 라는 단어가 다른 개념으로 쓰이니, 얼마나 헷갈리는지 느껴볼 수 있다.
적절한 "최대 스레드"의 갯수
두가지 관점에서 볼 수 있게 된다.
CPU Bound 작업
- CPU의 코어 수(하이퍼 스레딩 가능한 갯수) 와 동일한 스레드를 갖는 것이 좋음
- 추가로 1개의 비상용 작업 스레드가 있으면 좋을 듯
- 갑자기 일찍 끝나서 CPU의 코어 하나가 놀 때를 대비한!
- 어차피 CPU가 빡시게 구르기 때문에, 컨텍스트 스위칭을 할 필요가 없다.
I/O Bound 작업 - 주로 WAS 가 수행하는 작업
- CPU를 최대한 사용할 수 있는 숫자까지 스레드 생성한다.
- 단, 스레드를 많이 생성하면 할수록 컨텍스트 스위칭 비용도 함께 증가한다.
- 따라서 적절한 "성능 테스트" (jmeter, nGrinder) 가 필요하다고 할 수 있겠다.