# Thread Pool, Fork-Join

> 대부분의 현대 운영체제는 한 프로세스가 다중 스레드를 포함하는 특성을 제공함

\


## Multi-Thread

[스레드 참고](https://github.com/jmxx219/CS-Study/blob/main/OperatingSystem/process.md#%EC%8A%A4%EB%A0%88%EB%93%9Cthread)

* 개념
  * 하나의 프로세스 내에서 둘 이상의 스레드가 자원을 공유하며 동시에 작업을 수행하는 것
    * 하나의 프로그램에서 두 가지 이상의 동작을 동시에 처리할 수 있음
  * 웹 서버는 대표적인 멀티 스레드 응용 프로그램
    * ex. 사용자가 서버 데이터베이스에 자료를 요청하는 동안 브라우저의 다른 기능을 이용할 수 있음
* 장점
  * 스레드는 프로세스보다 가벼움
    * 프로세스와 달리 코드, 데이터, 스택 영역을 제외한 나머지 자원을 서로 공유하기 때문에 기본적으로 내장되어 있는 데이터 용량이 프로세스보다 작음
  * 자원의 효율성 증대
    * 스레드 간에 자원 공유가 가능해 프로세스 간 통신(IPC)을 사용하지 않고도 데이터 공유가 가능함
  * 프로세스 보다 Context Switching 비용 감소
    * 프로세스(CPU 캐시 정보 모두 교체), 스레드(공유 자원 제외한 정보만 교체)
  * 높은 응답성(응답 시간 단축)
* 문제점
  * 안정성 문제
    * `하나의 스레드에서 문제가 발생하면 다른 스레드도 영향을 받아 전체 프로그램이 종료될 수 있음`
    * 해결 방법: 프로그래머의 역량에 따라 극복 가능
      * 적절한 예외 처리
      * 에러 발생 시, 새로운 스레드 생성 or 스레드 풀(Thread Pool) 잔여 스레드 이용 등
  * 스레드 생성 문제(요청이 올 때마다 새로운 스레드 생성)
    * 스레드 생성 비용(비쌈) + 컨텍스트 스위치 비용 발생
    * 요청이 들어올 때마다 만들면 요청에 대한 응답 속도 느려짐
    * 생성 제한 없이, 스레드를 무한정 생성하면 CPU 시간, 메모리 공간 등 시스템 자원이 고갈됨
    * 해결 방법: `스레드 풀(Thread Pool)`
  * 동기화 문제([멀티 스레드와 동기화 참고](https://github.com/jmxx219/CS-Study/blob/main/Java-Spring/Synchronized.md#%EB%A9%80%ED%8B%B0-%EC%8A%A4%EB%A0%88%EB%93%9Cmulti-thread))
    * 자원을 공유하기 때문에 동기화 문제가 발생함
    * 해결 방법: 뮤텍스, 세마포어, 모니터(Monitor) 등

\


## 스레드 풀(Thread Pool)

* 개념
  * 프로세스를 시작할 때 일정한 수의 스레드를 미리 풀로 만들어두는 것
    * 풀(Pool): 필요할 때마다 개체를 할당하고 파괴하는 대신, 사용 준비된 상태로 초기화된 개체 집합
  * 해당 스레드들은 평소에 하는 일 없이 대기하고, 필요할 때 꺼내서 씀
  * 기존 스레드를 재사용해서 성능을 향상시키고 스레드 생성에 따른 오버헤드를 감소시킴
* 필요성
  * 프로그램 컨텍스트 스위치로 인한 성능저하를 방지하기 위해
    * 스레드 생성/소멸을 작업(요청)이 들어올 때마다 하면 오버헤드가 발생함
  * 다수의 사용자의 요청을 수용하고, 빠르게 처리하고 대응할 수 있음
    * 스레드 풀에 의해 라이프 사이클이 관리되고, 작업이 큐를 이용하게 되어 우선순위가 배분되고 처리됨
* 동작 방식
  1. 스레드의 최대 개수를 제한하고, 정의된 스레드 수로 스레드 풀 생성
  2. 요청이 오면 작업 큐에 넣고, 해당작업을 스레드 풀의 스레드가 맡아 처리함
     1. 작업 요청이 오면 스레드를 생성하는 것이 아닌 사용 가능한 스레드를 할당함
     2. 만약 모든 스레드가 사용 중이라면 사용 가능한 스레드가 생길 때까지 작업을 대기함
  3. 작업을 완료한 스레드는 다시 스레드 풀로 돌아가 대기함

\


#### 스레드 풀 장단점

* 장점
  * 리소스 관리
    * 스레드를 재사용함으로써 스레드 생성/소멸과 관련된 오버헤드를 줄여 효율적인 리소스 사용을 구현할 수 있음
    * 오버헤드 줄임
  * 로드 밸런싱
    * 사용 가능한 스레드 간 작업을 분산해서 개별 스레드가 과부하되는 것을 방지함
  * 스레드 생성보다 기존 스레드를 서비스하는 것이 종종 더 빠름
  * 스레드 개수 제한
    * 이 제한으로 많은 수의 스레드를 병렬 처리할 수 없는 시스템에 도움을 줌
    * 스레드 무한정 생성 방지
* 단점
  * 작업자가 스레드 개수가 충분하지 못하게 구성하면 리소스 경합이 발생해 성능 문제 발생
  * 스레드 최대 개수를 너무 많이 설정하고 사용하지 않으면 메모리 낭비가 발생함
  * 최적의 스레드 풀 크기 설정의 어려움
    * 특정 애플리케이션에 대해 최적의 스레드 풀 크기를 찾는 것은 여러 요인(사용자 수, 사용 환경, 성능 등)을 고려해야 하기 때문에 어려움
* 스레드 풀의 단점 개선 : `Fork Join Pool`

\


#### 스레드 수 결정

* CPU 코어의 수, 물리 메모리양, 동시 요청 클라이언트 최대 개수 등을 고려하여 정해질 수 있음
  * CPU 코어 수
    * 일반적으로 스레드 수를 대략 사용 가능한 CPU 코어 수와 비슷하게 유지함
    * 시스템이 사용 가능한 처리 능력을 최대한 활용할 수 있음
  * 작업의 성격에 따라
    * 계산 집약적(CPU-bound) 작업
      * CPU에서 연산을 수행하는 데 집중하는 작업 (예: 행렬 연산, 암호화, 머신러닝 연산 등)
      * 적절한 스레드 수: 코어 개수(or 코어 개수 + 1)
      * 이유:
        * CPU는 병렬 연산을 수행할 수 있지만, 너무 많은 스레드가 있으면 오히려 컨텍스트 스위칭 오버헤드가 발생하여 성능 저하가 발생함.
        * 보통 물리 코어 수 또는 (논리 코어 수 - 1) 정도가 최적
    * I/O 바운드(IO-bound) 작업
      * 네트워크 요청, 파일 읽기/쓰기, 데이터베이스 조회 등 CPU보다는 외부 자원을 기다리는 작업
      * 적절한 스레드 수: 코어 개수보다 많아도 됨 (최대 수는 실험적으로 조절)
      * 이유:
        * I/O 요청이 들어가면 해당 스레드는 블록되거나 비활성 상태가 됨.
        * 따라서 더 많은 스레드를 사용하면 CPU가 쉬는 동안 다른 스레드가 작업을 수행할 수 있어 효율적.
        * 하지만 너무 많으면 컨텍스트 스위칭 오버헤드 발생 → 적절한 개수를 찾아야 함.
  * 메모리 사용량
* 더 정교하게 할 경우, 풀의 활용도를 보며 동적으로 풀의 크기를 바꿀 수 있음
  * 시스템 부하가 적을 때에는 더 작은 풀을 유지할 수 있도록 함

\


## Fork-Join

![](https://velog.velcdn.com/images%2Flcy960729%2Fpost%2Fd33c47fb-6e24-4728-b271-cca7fa1ca27e%2Fimage.png)

* 병렬 처리를 위한 공통된 모델
  * 분할정복 알고리즘의 병렬화 버전
* 문제를 더 작은 하위 문제로 분할하고 병렬 실행을 위해서 여러 스레드에 분산시킴
  * 이후 하위 문제가 해결되면 결과를 결합하여 원래 문제를 해결함
* 메인 부모 스레드가 자식 스레드를 생성(fork)한 다음, 자식의 종료를 기다린 후 join하고 그 시점부터 자식의 결과를 확인하고 결합하는 방법
  * `fork()`: 프로세스(작업)을 여러 개로 쪼개서 새롭개 생성하는 작업
  * `join()`: 포크해서 생성된 프로세스/스레드의 결과를 합치는 작업

\


### Fork Join Pool(Fork Join Framework)

![](https://miro.medium.com/v2/resize:fit:1400/0*1zJu8l6yZhY2uWjI)

* 개념
  * Java 7부터 사용가능한 Java Concurrency 툴
    * 정확히는 Fork Join Framework이고, ForkJoinPool은 대표 클래스임
      * ForkJoinPool: fork-join task를 실행하는 thread pool
  * 기존 스레드 풀을 개선하기 위한 방법으로, 스레드 풀 안에서 개별 스레드들한테 업무를 분배하는 방식
* 동작 방식
  1. 큰 업무를 작은 단위의 업무로 분할
  2. 부모 쓰레드로부터 처리 로직을 복사하여 새로운 쓰레드에서 분할된 업무를 수행(Fork)시킴
  3. Fork를 반복하다가, 특정 쓰레드에서 더 이상 Fork가 일어나지 않고 업무가 완료되면 그 결과를 부모 쓰레드에서 Join하여 값을 취합함
  4. 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를 가짐

\
