# Thread Pool ![image](https://hackmd.io/_uploads/HJa51FYu3.png) - 쓰레드 풀(thread pool)은 병렬 컴퓨팅에 사용되는 소프트웨어 디자인 패턴이다. - 이 패턴은 프로그램이 프로그램의 수명 주기 동안 생성하고 소멸하는 쓰레드의 수를 미리 결정함으로써 일반적인 비용을 최소화하는 데 중점을 둔다 - 쓰레드 풀은복잡한 작업을 여러 쓰레드로 분할하여 동시에 실행할 수 있으므로, 멀티코어 프로세서의 이점을 최대한 활용할 수 있다. - 쓰레드 풀은 고성능 컴퓨팅, 웹 서버, 데이터베이스, 그래픽 렌더링 등 많은 컴퓨팅 작업에서 활용된다. - 쓰레드 풀은 프로그램의 초기화 단계에서 생성되며, 이는 프로그램이 요구에 따라 쓰레드를 생성하거나 종료하는 데 드는 비용을 절감하고, 쓰레드의 최대 수를 미리 결정함으로써 시스템 자원의 과다한 사용을 방지한다. - 쓰레드 풀에서는 일반적으로 다음과 같은 작업을 수행한다 1. **쓰레드 생성**: 쓰레드 풀은 프로그램 시작 시에 일정 수의 쓰레드를 생성한다. 이렇게 미리 생성되는 쓰레드의 수는 풀의 최소 크기를 결정한다 2. **작업 할당**: 쓰레드 풀은 프로그램에서 실행해야 하는 작업을 받아들이고, 이를 쓰레드에 할당한다. 일반적으로 이 작업은 큐(queue)나 스택(stack) 같은 자료구조를 통해 관리된다. 3. **쓰레드 관리**: 쓰레드 풀은 풀의 현재 크기와 작업의 수를 기반으로 필요한 경우 추가적인 쓰레드를 생성하거나, 반대로 필요하지 않은 쓰레드를 종료할 수 있다. 이는 동적으로 시스템의 로드를 관리하는 데 도움이 된다. 4. **쓰레드 재사용**: 쓰레드 풀에서는 일단 작업이 완료된 쓰레드를 종료하지 않고, 다음 작업에 재사용한다. 이는 쓰레드 생성과 소멸에 대한 비용을 크게 줄일 수 있다. ## 직접 만들어보는 Thread Pool ```java import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedDeque; public class CustomThreadPool { private final List<Thread> threads = new ArrayList<>(); private final Queue<Runnable> queue = new ConcurrentLinkedDeque<>(); public CustomThreadPool(int poolSize) { for (int i = 0; i < poolSize; i++) { Thread thread = new Thread(() -> { while (true) { try { Runnable job = queue.poll(); if (job != null) { job.run(); } else { Thread.sleep(20); } } catch (Exception e) { e.printStackTrace(); } } }); threads.add(thread); thread.start(); } } public void execute(Runnable task) { queue.add(task); } public static void main(String[] args) { CustomThreadPool pool = new CustomThreadPool(5); for (int i = 0; i < 10; i++) { int nowCount = i; pool.execute(() -> System.out.println("Task " + nowCount + " is running.")); } } } ``` ## ExecutorService ![image](https://hackmd.io/_uploads/SJ3ltMaJC.gif) - 병렬 작업 시 여러개의 작업을 효율적으로 처리하기 위해 제공되는 자바 라이브러리 - 흔히 말하는 ThreadPool구현을 매우 용이하게 할 수 있으므로, Java에서 스레드 풀을 사용하고자 할 때 사용한다 - Runnable, Callable 중 하나를 상속하여 구현한 클래스를 인자로 받아서 처리할 수 있다. - Runnable : return 값이 없는 스레드 - Callable : return 값이 있는 스레드 ### ExecutorService의 종류 - `CachedThreadPool` : 스레드를 캐싱하는 스레드 풀. 필요할 때 필요한 만큼 스레드 풀을 생성한다. 이미 생성된 스레드를 재활용할 수 있기 때문에 성능상의 이점이 있을 수 있다. 다만 스레드 수가 폭발적으로 증가할 수 있다. (여기서 쓰이는 캐싱의 의미는 일정시간동안 스레드를 검사한다는 뜻이다. 60초동안 작업이 없으면 Pool에서 제거한다) - `Executors.newCachedThreadPool()` - `ScheduledThreadPool` : 일정 시간 뒤에 실행되는 작업이나 주기적으로 수행되는 작업에 사용된다. - `Executors.newScheduledThreadPool()` - `FixedThreadPool` : 고정된 개수를 가진 스레드풀 - `Executors.newFixedThreadPool()` - `SingleThreadExecutor` : 한 개의 스레드로 작업을 처리하는 스레드풀. 스레드 풀이라기보단 TaskPool의 개념이 더 적합하다 - `Executors.newSingleThreadExecutor()` ### 메서드 - `submit()` : 작업을 추가할 땐 `submit` 메서드를 사용하면 된다 - `shutdown()` : 더 이상 쓰레드풀에 작업을 추가하지 못하도록 한다. 그리고 처리 중인 Task가 모두 완료되면 쓰레드풀을 종료시킨다. - `awaitTermination()` : 이미 수행 중인 Task가 지정된 시간동안 끝나기를 기다리고 지정된 시간 내에 끝나지 않으면 false를 리턴하며, 이 때 `shutdownNow()`를 호출하면 실행 중인 Task를 모두 강제로 종료시킬 수 있다. ### ExecutorService 예시 #### 객체 생성 ```java ExecutorService executor = Executors.newFixedThreadPool(4); ``` #### 작업 추가 ```java ExecutorService executor = Executors.newSingleThreadExecutor(); executor.submit(() -> { System.out.println("Do Something"); return null; }); ``` #### Sample 1 - `submit( () -> {} )`은 멀티스레드로 처리할 작업을 예약한다. 인자로 람다식을 전달할 수 있다. (Runnable, Callable) ```java public class ExecutorServiceTest { public static void main(String args[]) throws InterruptedException { ExecutorService executor = Executors.newFixedThreadPool(4); executor.submit(() -> { String threadName = Thread.currentThread().getName(); System.out.println("Job1 " + threadName); }); executor.submit(() -> { String threadName = Thread.currentThread().getName(); System.out.println("Job2 " + threadName); }); executor.submit(() -> { String threadName = Thread.currentThread().getName(); System.out.println("Job3 " + threadName); }); executor.submit(() -> { String threadName = Thread.currentThread().getName(); System.out.println("Job4 " + threadName); }); // 더이상 ExecutorService에 Task를 추가할 수 없습니다. // 작업이 모두 완료되면 쓰레드풀을 종료시킵니다. executor.shutdown(); // shutdown() 호출 전에 등록된 Task 중에 아직 완료되지 않은 Task가 있을 수 있습니다. // Timeout을 20초 설정하고 완료되기를 기다립니다. // 20초 전에 완료되면 true를 리턴하며, 20초가 지나도 완료되지 않으면 false를 리턴합니다. if (executor.awaitTermination(20, TimeUnit.SECONDS)) { System.out.println(LocalTime.now() + " All jobs are terminated"); } else { System.out.println(LocalTime.now() + " some jobs are not terminated"); // 모든 Task를 강제 종료합니다. executor.shutdownNow(); } System.out.println("end"); } } ``` ```log Job1 pool-1-thread-1 Job3 pool-1-thread-1 Job4 pool-1-thread-1 Job2 pool-1-thread-2 end ``` #### Sample 2 - `newSingleThreadExecutor` 를 사용하면 순차적으로 처리된다. ```java public class ExecutorServiceTest2 { public static void main(String args[]) throws InterruptedException { ExecutorService executor = Executors.newSingleThreadExecutor(); executor.submit(() -> { String threadName = Thread.currentThread().getName(); System.out.println("Job1 " + threadName); }); executor.submit(() -> { String threadName = Thread.currentThread().getName(); System.out.println("Job2 " + threadName); }); executor.submit(() -> { String threadName = Thread.currentThread().getName(); System.out.println("Job3 " + threadName); }); executor.submit(() -> { String threadName = Thread.currentThread().getName(); System.out.println("Job4 " + threadName); }); executor.shutdown(); executor.awaitTermination(20, TimeUnit.SECONDS); System.out.println("end"); } } ``` ```log Job1 pool-1-thread-1 Job2 pool-1-thread-1 Job3 pool-1-thread-1 Job4 pool-1-thread-1 end ```