출처 - https://github.com/jmxx219/CS-Study (opens in a new tab)
Thread Pool, Fork-Join
대부분의 현대 운영체제는 한 프로세스가 다중 스레드를 포함하는 특성을 제공함
Multi-Thread
- 개념
- 하나의 프로세스 내에서 둘 이상의 스레드가 자원을 공유하며 동시에 작업을 수행하는 것
- 하나의 프로그램에서 두 가지 이상의 동작을 동시에 처리할 수 있음
- 웹 서버는 대표적인 멀티 스레드 응용 프로그램
- ex. 사용자가 서버 데이터베이스에 자료를 요청하는 동안 브라우저의 다른 기능을 이용할 수 있음
- 하나의 프로세스 내에서 둘 이상의 스레드가 자원을 공유하며 동시에 작업을 수행하는 것
- 장점
- 스레드는 프로세스보다 가벼움
- 프로세스와 달리 코드, 데이터, 스택 영역을 제외한 나머지 자원을 서로 공유하기 때문에 기본적으로 내장되어 있는 데이터 용량이 프로세스보다 작음
- 자원의 효율성 증대
- 스레드 간에 자원 공유가 가능해 프로세스 간 통신(IPC)을 사용하지 않고도 데이터 공유가 가능함
- 프로세스 보다 Context Switching 비용 감소
- 프로세스(CPU 캐시 정보 모두 교체), 스레드(공유 자원 제외한 정보만 교체)
- 높은 응답성(응답 시간 단축)
- 스레드는 프로세스보다 가벼움
- 문제점
- 안정성 문제
하나의 스레드에서 문제가 발생하면 다른 스레드도 영향을 받아 전체 프로그램이 종료될 수 있음
- 해결 방법: 프로그래머의 역량에 따라 극복 가능
- 적절한 예외 처리
- 에러 발생 시, 새로운 스레드 생성 or 스레드 풀(Thread Pool) 잔여 스레드 이용 등
- 스레드 생성 문제(요청이 올 때마다 새로운 스레드 생성)
- 스레드 생성 비용(비쌈) + 컨텍스트 스위치 비용 발생
- 요청이 들어올 때마다 만들면 요청에 대한 응답 속도 느려짐
- 생성 제한 없이, 스레드를 무한정 생성하면 CPU 시간, 메모리 공간 등 시스템 자원이 고갈됨
- 해결 방법:
스레드 풀(Thread Pool)
- 동기화 문제(멀티 스레드와 동기화 참고 (opens in a new tab))
- 자원을 공유하기 때문에 동기화 문제가 발생함
- 해결 방법: 뮤텍스, 세마포어, 모니터(Monitor) 등
- 안정성 문제
스레드 풀(Thread Pool)
- 개념
- 프로세스를 시작할 때 일정한 수의 스레드를 미리 풀로 만들어두는 것
- 풀(Pool): 필요할 때마다 개체를 할당하고 파괴하는 대신, 사용 준비된 상태로 초기화된 개체 집합
- 해당 스레드들은 평소에 하는 일 없이 대기하고, 필요할 때 꺼내서 씀
- 기존 스레드를 재사용해서 성능을 향상시키고 스레드 생성에 따른 오버헤드를 감소시킴
- 프로세스를 시작할 때 일정한 수의 스레드를 미리 풀로 만들어두는 것
- 필요성
- 프로그램 컨텍스트 스위치로 인한 성능저하를 방지하기 위해
- 스레드 생성/소멸을 작업(요청)이 들어올 때마다 하면 오버헤드가 발생함
- 다수의 사용자의 요청을 수용하고, 빠르게 처리하고 대응할 수 있음
- 스레드 풀에 의해 라이프 사이클이 관리되고, 작업이 큐를 이용하게 되어 우선순위가 배분되고 처리됨
- 프로그램 컨텍스트 스위치로 인한 성능저하를 방지하기 위해
- 동작 방식
- 스레드의 최대 개수를 제한하고, 정의된 스레드 수로 스레드 풀 생성
- 요청이 오면 작업 큐에 넣고, 해당작업을 스레드 풀의 스레드가 맡아 처리함
- 작업 요청이 오면 스레드를 생성하는 것이 아닌 사용 가능한 스레드를 할당함
- 만약 모든 스레드가 사용 중이라면 사용 가능한 스레드가 생길 때까지 작업을 대기함
- 작업을 완료한 스레드는 다시 스레드 풀로 돌아가 대기함
스레드 풀 장단점
장점
- 리소스 관리
- 스레드를 재사용함으로써 스레드 생성/소멸과 관련된 오버헤드를 줄여 효율적인 리소스 사용을 구현할 수 있음
- 오버헤드 줄임
- 로드 밸런싱
- 사용 가능한 스레드 간 작업을 분산해서 개별 스레드가 과부하되는 것을 방지함
- 스레드 생성보다 기존 스레드를 서비스하는 것이 종종 더 빠름
- 스레드 개수 제한
- 이 제한으로 많은 수의 스레드를 병렬 처리할 수 없는 시스템에 도움을 줌
- 스레드 무한정 생성 방지
단점
- 작업자가 스레드 개수가 충분하지 못하게 구성하면 리소스 경합이 발생해 성능 문제 발생
- 스레드 최대 개수를 너무 많이 설정하고 사용하지 않으면 메모리 낭비가 발생함
- 최적의 스레드 풀 크기 설정의 어려움
- 특정 애플리케이션에 대해 최적의 스레드 풀 크기를 찾는 것은 여러 요인(사용자 수, 사용 환경, 성능 등)을 고려해야 하기 때문에 어려움
- 스레드 풀의 단점 개선 :
Fork Join Pool
스레드 수 결정
- CPU 코어의 수, 물리 메모리양, 동시 요청 클라이언트 최대 개수 등을 고려하여 정해질 수 있음
- CPU 코어 수
- 일반적으로 스레드 수를 대략 사용 가능한 CPU 코어 수와 비슷하게 유지함
- 시스템이 사용 가능한 처리 능력을 최대한 활용할 수 있음
- 작업의 성격에 따라
- 작업이 계산 집약적이고 대기 시간이 크지 않은 경우: 더 많은 스레드가 유리
- I/O 작업이나 대기가 많은 작업의 경우: 더 적은 수의 스레드로도 충분할 수 있음
- 메모리 사용량
- CPU 코어 수
- 더 정교하게 할 경우, 풀의 활용도를 보며 동적으로 풀의 크기를 바꿀 수 있음
- 시스템 부하가 적을 때에는 더 작은 풀을 유지할 수 있도록 함
Fork-Join
- 병렬 처리를 위한 공통된 모델
- 분할정복 알고리즘의 병렬화 버전
- 문제를 더 작은 하위 문제로 분할하고 병렬 실행을 위해서 여러 스레드에 분산시킴
- 이후 하위 문제가 해결되면 결과를 결합하여 원래 문제를 해결함
- 메인 부모 스레드가 자식 스레드를 생성(fork)한 다음, 자식의 종료를 기다린 후 join하고 그 시점부터 자식의 결과를 확인하고 결합하는 방법
fork()
: 프로세스(작업)을 여러 개로 쪼개서 새롭개 생성하는 작업join()
: 포크해서 생성된 프로세스/스레드의 결과를 합치는 작업
Fork Join Pool(Fork Join Framework)
- 개념
- Java 7부터 사용가능한 Java Concurrency 툴
- 정확히는 Fork Join Framework이고, ForkJoinPool은 대표 클래스임
- ForkJoinPool: fork-join task를 실행하는 thread pool
- 정확히는 Fork Join Framework이고, ForkJoinPool은 대표 클래스임
- 기존 스레드 풀을 개선하기 위한 방법으로, 스레드 풀 안에서 개별 스레드들한테 업무를 분배하는 방식
- Java 7부터 사용가능한 Java Concurrency 툴
- 동작 방식
- 큰 업무를 작은 단위의 업무로 분할
- 부모 쓰레드로부터 처리 로직을 복사하여 새로운 쓰레드에서 분할된 업무를 수행(Fork)시킴
- Fork를 반복하다가, 특정 쓰레드에서 더 이상 Fork가 일어나지 않고 업무가 완료되면 그 결과를 부모 쓰레드에서 Join하여 값을 취합함
- Join을 반복하다가, 최초에 ForkJoinPool을 생성한 쓰레드로 값을 리턴하여 작업을 완료함
- Work Stealing(작업 훔치기)
- ForkJoinPool의 모든 스레드를 공정하게 분할하기 위한 방법
- task queue로 dequeue를 이용함
- 다른 스레드는 바쁘게 일하고 한 스레드는 할 일이 없어졌을 때, 다른 스레드 큐의 꼬리에서 작업을 훔쳐와서 작업을 처리함
ThreadPool vs ForkJoinPool
- ThreadPool은 Thread의 생성 비용을 줄이기 위해, Thread를 갖고 있는 역할
- ForkJoinPool은 ThreadPool처럼 ForkJoinTask를 갖고 있는 역할을 하면서도, 각 스레드에 분담한 업무(task)를 다른 스레드에 훔쳐오는(work stealing) 역할을 함
- 놀고있는 쓰레드를 방지함
- ThreadPool은 Runnable, Callable 객체를 갖지만, ForkJoinPool은 Runnable, Callable 객체를 한번 wrapping한 ForkJoinTask를 가짐