|
| 1 | +# 섹션 13. 스레드 풀과 Executor 프레임워크1 |
| 2 | + |
| 3 | +## 스레드를 직접 사용할 때의 문제점 |
| 4 | + |
| 5 | +- 스드 생성 시간으로 인한 성능 문제 |
| 6 | +- 스레드 관리 문제 |
| 7 | +- `Runnable` 인터페이스의 불편함 |
| 8 | + |
| 9 | +### 1. 스레드 생성 비용으로 인한 성능 문제 |
| 10 | + |
| 11 | +스레드는 매우 무거움 |
| 12 | + |
| 13 | +- 메모리 할당: 각 스레드는 자신만의 호출 스택 보유, 즉 생성 시 메모리 할당을 해야함 |
| 14 | +- 운영체제 자원 사용: 스레드 생성은 운영체제 커널 수준에서 시스템 콜을 통해 이루어짐 → CPU, 메모리 자원 사용 |
| 15 | +- 운영체제 스케줄러 설정: 스레드가 새로 생성되면 스케줄러는 이 스레드를 관리 및 순서 조정을 해야함 → 오버헤드 발생 |
| 16 | + |
| 17 | +> 스레드 재사용을 하면 해당 문제를 해결할 수 있음 |
| 18 | +> |
| 19 | +
|
| 20 | +### 2. 스레드 관리 문제 |
| 21 | + |
| 22 | +CPU와 메모리 자원은 한정되어 있음 → 스레드는 무한이 아님 |
| 23 | + |
| 24 | +시스템이 버틸 수 있는 최대 스레드의 수 까지만 스레드를 생성할 수 있도록 관리 필요 |
| 25 | + |
| 26 | +### 3. Runnable 인터페이스의 불편함 |
| 27 | + |
| 28 | +```java |
| 29 | +public interface Runnable { |
| 30 | + void run(); |
| 31 | +} |
| 32 | +``` |
| 33 | + |
| 34 | +- 반환 값이 없다: run() 메서드는 반환 값을 갖지 않음 → 실행 결과를 얻기 위해 별도 메커니즘 필요 |
| 35 | +- 예외 처리: run() 메서드는 체크 예외(checked exception)를 던질 수 없다. 메서드 내부에서 처리 |
| 36 | + |
| 37 | +### 해결 |
| 38 | + |
| 39 | +1, 2번 문제를 해결하기 위해서는 스레드를 생성, 관리 할 풀(Pool)이 필요 |
| 40 | + |
| 41 | +- 스레드를 관리하는 스레드 풀에 스레드를 미리 만듦 |
| 42 | +- 스레드는 스레드 풀에서 대기 |
| 43 | +- 작업 요청이 오면, 스레드 풀에서 스레드 하나를 조회 |
| 44 | +- 조회한 스레드로 작업 처리 |
| 45 | +- 작업이 완료되면 스레드 종료가 아닌 스레드 풀에 반환 |
| 46 | + |
| 47 | +스레드 풀을 사용하면 재사용이 가능해져 생성 시간 및 관리가 용이해짐 |
| 48 | + |
| 49 | +처리할 작업이 없으면 스레드는 `WAITING` 요청이 오면 `RUNNABLE` |
| 50 | + |
| 51 | +## Executor 프레임워크 소개 |
| 52 | + |
| 53 | +스레드 풀의 유지관리를 위해 스레드의 상태 전이 및 생산자 소비자 문제와 같은 문제를 해결해주는 프레임 워크 |
| 54 | + |
| 55 | +개발자가 직접 스레드를 생성하고 관리하지 않고 효율적으로 처리하게 도와줌 |
| 56 | + |
| 57 | +### Executor 인터페이스 |
| 58 | + |
| 59 | +```java |
| 60 | +package java.util.concurrent; |
| 61 | + public interface Executor { |
| 62 | + void execute(Runnable command); |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +### ExecutorService 인터페이스 |
| 67 | + |
| 68 | +```java |
| 69 | +public interface ExecutorService extends Executor, AutoCloseable { |
| 70 | + <T> Future<T> submit(Callable<T> task); |
| 71 | + @Override |
| 72 | + default void close(){...} |
| 73 | + ... |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +- Executor 프레임워크를 사용할 때 대부분 이 인터페이스 사용 |
| 78 | + |
| 79 | +`ExecutorService` 인터페이스의 기본 구현체 → `ThreadPoolExecutor` |
| 80 | + |
| 81 | +### ThreadPoolExecutor 생성자 |
| 82 | + |
| 83 | +- corePoolSize: 스레드 풀에서 관리되는 기본 스레드의 수 |
| 84 | +- maximumPoolSize: 스레드 풀에서 관리되는 최대 스레드의 수 |
| 85 | +- keepAliveTime, TimeUnit unit: 기본 스레드 수를 초과해서 만들어진 스레드가 생존할 수 있는 대기시간, 초과시 제거 |
| 86 | +- BlockingQueue workQueue: 작업을 보관할 블로킹 큐 |
| 87 | + |
| 88 | +```java |
| 89 | +12:10:54.451 [main] == 초기 상태 == |
| 90 | +12:10:54.461 [main] [pool=0, active=0, queuedTasks=0, completedTasks=0] main]==작업수행중== |
| 91 | +12:10:54.461 [main] == 작업수행중 == |
| 92 | +12:10:54.461 [main] [pool=2, active=2, queuedTasks=2, completedTasks=0] |
| 93 | +12:10:54.461 [pool-1-thread-1] taskA 시작 |
| 94 | +12:10:54.461 [pool-1-thread-2] taskB 시작 |
| 95 | +12:10:55.467 [pool-1-thread-1] taskA 완료 |
| 96 | +12:10:55.467 [pool-1-thread-2] taskB 완료 |
| 97 | +12:10:55.468 [pool-1-thread-1] taskC 시작 |
| 98 | +12:10:55.468 [pool-1-thread-2] taskD 시작 |
| 99 | +12:10:56.471 [pool-1-thread-2] taskD 완료 |
| 100 | +12:10:56.474 [pool-1-thread-1] taskC 완료 |
| 101 | +12:10:57.465 [main] == 작업수행완료 == |
| 102 | +12:10:57.466 [main] [pool=2, active=0, queuedTasks=0, completedTasks=4] |
| 103 | +12:10:57.469 [main] == shutdown 완료 == |
| 104 | +12:10:57.468 [main] [pool=0, active=0, queuedTasks=0, completedTasks=4] |
| 105 | +``` |
| 106 | + |
| 107 | +1. 초기 상태 시점에는 스레드 풀에 스레드를 미리 만들지 않음 |
| 108 | +2. 메인 스레드가 스레드 풀에 execute로 작업 호출 |
| 109 | +3. 작업 요청이 들어오면 처리하기 위해 스레드를 만든다. |
| 110 | +4. 작업이 들어올 때마다 corePoolSize까지 스레드 생성 |
| 111 | +5. 작업이 완료되면 스레드 풀에 스레드 반납, `WAITING` 상태로 대기 |
| 112 | +6. 반납된 스레드는 재사용 |
| 113 | +7. close() 호출 시 ThreadPoolExecutor 종료, 스레드 풀의 스레드 제거 |
| 114 | + |
| 115 | +## Future |
| 116 | + |
| 117 | +**Runnable** |
| 118 | + |
| 119 | +```java |
| 120 | +public interface Runnable { |
| 121 | + void run(); |
| 122 | +} |
| 123 | +``` |
| 124 | + |
| 125 | +- Runnable의 run()은 반환 타입이 void → 값 반환 불가 |
| 126 | +- 예외가 선언되어 있지 않음 → 해당 인터페이스의 구현체는 모두 체크 예외를 던질 수 없음 |
| 127 | + - 런타임은 제외 |
| 128 | + |
| 129 | +**Callable** |
| 130 | + |
| 131 | +```java |
| 132 | +public interface Callable<V> { |
| 133 | + V call() throws Exception; |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +- java.util.concurrent에서 제공 |
| 138 | +- Callable의 call()의 반환 타입은 제네릭 V → 값 반환 가능 |
| 139 | +- 예외가 선언 되어있으므로 체크 예외를 던질 수 있음 |
| 140 | + |
| 141 | +Callable은 다음과 같이 결과 값을 받을 수 있음 |
| 142 | + |
| 143 | +```java |
| 144 | +Future<Integer> future = es.submit(new MyCallable()); |
| 145 | +Integer result = future.get(); |
| 146 | +``` |
| 147 | + |
| 148 | +그런데 MyCallable은 즉시 실행되어 결과를 반환하는 것이 불가능 함 → 다른 스레드에서 처리되기 때문 |
| 149 | + |
| 150 | +따라서 es.submit은 결과 대신 Future 객체를 반환 |
| 151 | + |
| 152 | +### Future 분석 |
| 153 | + |
| 154 | +- es.submit(taskA) 호출을 통해 taskA의 미래 결과를 알 수 있는 `Future` 객체 생성 |
| 155 | +- Future 객체 안에 taskA의 인스턴스 보관 |
| 156 | + - 내부에 taskA의 작업 완료 여부, 결과 보관 |
| 157 | +- ThreadPoolExecutor의 블로킹 큐로 taskA가 아닌 Future 객체가 들어감 |
| 158 | + - submit을 통해 작업을 전달할 때 생성된 Future은 즉시 반환됨 |
| 159 | +- 큐에 들어있던 Future을 꺼내 스레드 풀의 스레드가 작업 수행 |
| 160 | + - FutureTask.run() → MyCallable.call() |
| 161 | +- Future.get()을 통해 결과를 받을 수 있음 |
| 162 | + - 완료 된 상태: 값을 즉시 반환 |
| 163 | + - 미완료 상태: 요청한 스레드는 결과를 얻을 때 까지 대기(Blocking) → Thread.join()과 유사 |
| 164 | + - Future을 처리하던 스레드가 요청한 스레드를 깨움 |
| 165 | + |
| 166 | +> Future를 사용하면 마치 멀티스레드를 사용하지 않고, 단일 스레드 상황에서 메서드를 호출하고 결과를 받는 것 같이 사용가능 + 예외도 던질 수 있음 |
| 167 | +> |
| 168 | +
|
| 169 | +### Future 이유 |
| 170 | + |
| 171 | +- Future 없이 직접 반환: task1을 ExecutorService에 요청하고 결과 기다리고, task2를 요청하고 결과 기다리고 → 단일 스레드와 다른게 없음 |
| 172 | +- Future 반환: task1, task2를 각 스레드에 요청을 던져두고, 결과를 받을 때만 대기 → 대기시간을 공유하므로 절약할 수 있음 |
| 173 | + |
| 174 | +### Future 취소 |
| 175 | + |
| 176 | +cancel()의 매개변수에 따른 동작 차이 |
| 177 | + |
| 178 | +- cancel(true): Future를 취소 상태로 변경, 작업이 실행중이면 Thread.interrupt()를 호출해 작업 중단 |
| 179 | +- cancel(false): Future를 취소 상태로 변경, 이미 실행 중인 작업은 중단X |
| 180 | + |
| 181 | +> 둘 다 취소는 되었기 때문에 값을 받을 수는 없음 |
| 182 | +> |
| 183 | +
|
| 184 | +### Future 예외 |
| 185 | + |
| 186 | +요청스레드: es.submit(new ExCallable())을 호출하여 작업 전달 |
| 187 | + |
| 188 | +작업 스레드: ExCallable을 실행 → IllegalStateException 발생 |
| 189 | + |
| 190 | +- 작업 스레드는 Future에 예외를 담는다 |
| 191 | +- 예외 발생 → Future의 상태 `FAILED` |
| 192 | + |
| 193 | +요청 스레드: future.get() 호출 |
| 194 | + |
| 195 | +- Future의 상태가 `FAILED` 면 ExecutionException 던짐 |
| 196 | +- 이 예외는 Future의 저장해둔 원본 예외 포함 |
| 197 | + |
| 198 | +> 마치 싱글 스레드의 일반적인 메서드를 호출하는 것과 같이 사용 가능 |
| 199 | +> |
| 200 | +
|
| 201 | +## ExecutorService 작업 컬렉션 처리 |
| 202 | + |
| 203 | +- invokeAll(): 한번에 여러 작업 제출, 모든 작업 완료 까지 대기 |
| 204 | +- invokeAny(): 한번에 여러 작업 제출, 가장 먼저 완료된 작업 반환 나머지 작업은 인터럽트로 취소 |
0 commit comments