본문 바로가기

ComputerScience/OS

[OS] 스레드 풀(thread pool)은 무엇인가?

Thread per request model

Request마다 하나의 Thread를 할당시켜 하나의 Request는 하나의 Thread가 처리하도록 동작하는 모델이다.

 

문제점

  • 만약 Thread per reqeust 모델의 동작 방식이 서버에 들어오는 요청마다 스레드를 새로 만들어서 처리하고, 처리가 끝난 스레드는 버리는 식으로 동작한다면 스레드 생성에 소요되는 시간 때문에 요청 처리가 더 오래 걸린다.
  • 처리 속도보다 더 빠르게 요청이 늘어나면 스레드가 계속 생성이 되어 메모리가 고갈되고, 컨텍스트 스위칭이 더 자주 발생한다.
  • 이는 CPU 오버헤드 증가로 CPU Time이 낭비가 된다.
  • 어느 순간 서버 전체가 응답 불가능한 상태에 빠진다.

Thread Pool

이미지 출처: https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/Thread_pool.svg/1200px-Thread_pool.svg.png

미리 정해진 개수만큼 Thread를 생성해 놓고, 내부적으로 관리하는 Queue로 Task가 들어오게 되면 Thread 들은 Task를 할당받는다. 요청 처리가 완료된 Thread는 버려지지 않고 재사용된다.

이는 스레드 생성 시간을 절약하고, 스레드가 무제한으로 생성되는 것을 방지한다.

 

사례: 여러 작업을 동시에 처리해야 할 경우

  • Thread Per Request 모델의 경우
  • Task를 subtask로 나뉘어서 동시에 처리하는 경우
  • 순서 상관없이 동시 실행 가능한 Task 처리의 경우

몇 개의 스레드를 만들어 두는 게 적절한가?

-> CPU의 코어 개수와 task의 성향에 따라 다르다.

  • CPU-bound task라면 코어 개수 또는 그 이상을 만든다.
  • I/O bound task라면 코어 개수보다 1.5배~ 약 3배 이는 경험적으로 찾아야한다.

Queue에 요청이 무한정 쌓인다면? 그리고 Queue 사이즈의 제한이 없다면?

-> 이는 메모리 고갈의 원인이 될 수 있다.

 

자바의 ThreadPool 구현

Executors 클래스는 static 메서드로 다양한 형태의 스레드 풀을 제공한다.

아래 코드는 Executors가 ThreadPool을 생성하는 메서드 구현 일부이다.

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

파라미터로 nThreads는 몇 개의 쓰레드를 만들어 둘 것인지를 의미하고,

LinkedBlockingQueue는 스레드 풀에서 사용할 Queue이다.

 

아래는 LinkedBlockingQueue.java의 코드 일부이다.

public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    /**
     * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
     *
     * @param capacity the capacity of this queue
     * @throws IllegalArgumentException if {@code capacity} is not greater
     *         than zero
     */
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

여기서 생성자에 Integer.MAX_VALUE는 Queue의 Size를 의미하는데, 사실 상 20억이 넘는 사이즈이므로 처리해야 할 Task 들이 쌓이게 되면 메모리 고갈의 잠재적 위험이 있을 수 있다.

 

Python의 ThreadPoolExecutor 

https://github.com/python/cpython/blob/main/Lib/concurrent/futures/thread.py

여기서 SimpleQueue는 unbounded FIFO Queue이므로, 사이즈 제한이 없다.

사이즈 제한이 없는 Queue는 Thread의 Task 처리 속도 보다 Queue에 Task가 쌓이는 속도가 더 빠를 경우, 메모리를 고갈시키는 잠재적 위험 요인을 가진다.

따라서 Queue 크기를 제한을 할 필요가 있다.

번외: Connection Pool?

TCP Connection 또는 Database Connection 같은 Connection을 생성하는 것은 시간이 소요될 수 있다.

왜냐하면 Connection 생성 과정에서 네트워크 연결, 인증, 권한 확인 등 여러 단계를 거쳐야 하기 때문이다.

이 또한 Pool에 미리 만들어 두어 요청이 들어오면 빨리 처리할 수 있도록 한다.

예를 들어, DB Connection의 경우 DB의 Host, Port, 그리고 Connection TimeOut 설정과 같은 정보들을 포함할 수 있다.

 

참고로 Connection이란 애플리케이션과 외부 시스템, 일반적으로 데이터베이스 또는 서버 간의 통신 채널을 말한다.

번외 2: Process Pool

프로세스를 미리 만들어 놓고 저장해놓는 Pool이다.

주로 Python에서 사용될 수 있는데, Python은 GIL(Global Interpreter Lock)이라는 장치 때문에 동시에 여러 쓰레드가 CPU에 실행될 수 없다.

따라서 CPU의 멀티 코어를 사용하여 CPU bounded 프로세스를 동시에 실행하려면, 이 Pool을 사용해야한다.

참고