
ForkJoinPool vs WorkerThreadPool
공통점
두 개의 쓰레드 풀은 모두 작업을 병렬로 처리하기 위한 구조를 가지고 있습니다다.
1. 분할 정복 (Divide & Conquer)
- 큰 작업을 독립적인 작은 작업들로 분할하여 처리할 수 있습니다.
2. Task Container (작업 컨테이너)
- 분할된 Task를 저장하는 버퍼(큐)를 가지고 있습니다.
3. 쓰레드 재활용
- Task가 계속해서 실행되도록, 쓰레드를 유지하고 재사용합니다.
차이점
| 비교 항목 | ForkJoinPool (FJ) | WorkerThreadPool (WT) |
|---|
| 컨테이너 구조 | 쓰레드별 개별 큐 | 모든 쓰레드가 공유하는 큐 |
| 큐 방식 | LIFO(스택) | FIFO(큐) |
| 쓰레드 설정 | 직접 설정 불가능 | 사용자가 직접 개수 설정 가능 |
결론적으로 ForkJoinPool은 제어가 어렵고 활용성이 제한적이라 많이 사용되지 않습니다.
그러나 Apache Lucene 같은 일부 프로젝트에서는 의미 있게 사용됩니다.
반제어 동시성 모델
동시성 제어 방식 비교
1. 완전 제어 (동기적 프로그래밍)
- 함수 실행이 완료된 후에만 다음 코드가 실행됩니다
2. 제어 상실 (비동기적 프로그래밍)
- 함수 실행 중에 콜백을 넘기고, 특정 시점에 호출됩니다
3. 반제어 모델 (콜백을 원할 때만 넘김)
- 콜백을 제어할 최소한의 권한을 가집니다.
- Promise, CompletableFuture 등이 대표적 입니다.
CompletableFuture 흐름
- CompletableFuture 객체를 생성하면 내부에서 Task 실행이 시작됨.
- thenApply(callback)을 통해 콜백을 등록하면 Task 완료 후 실행됨.
- Task 완료 전후로 callback을 지정할 수 있으며, 동일한 Task에 여러 번 callback을 전달하면 캐싱 가능.
- Task 실행 쓰레드와 callback 실행 쓰레드를 분리 가능.
CPS (Continuation Passing Style) 패턴
기본 개념
- 함수 내부의 코드 흐름을 인자의 상태에 따라 여러 개로 분기하는 방식 입니다
- 실행 중인 함수의 중간 상태를 저장하고, 이후 실행될 함수에 전달하는 방식 입니다.
- 예를 들어 함수를 여러 개 선언하는 대신, 하나의 함수에서 상태를 기준으로 다르게 동작하도록
설계 할수 있습니다.
- 컨티뉴에이션 객체 = 함수의 분기 상태 + 그 시점의 반환값을 포함한 객체 입니다.
CPS 특징
- 루틴(일반 함수)은 호출되면 한 번 실행 후 종료되지만, CPS 방식은 이전 실행 상태를 저장하여 다음 호출에 활용 가능합니다.
- CPS 기반의 함수 호출을 반복하면 결국 코루틴(Coroutine)과 유사한 효과를 얻을 수 있습니다.
CPS + 워커쓰레드 패턴 결합
원리
- CPS를 활용하여 함수 호출을 Task로 변환합니다.
- 각 함수 호출이 별도의 Task로 실행되며, 이전 상태(Continuation)를 다음 Task에 전달합니다.
- 매번 다른 쓰레드에서 Task를 실행할 수도 있고, Task 실행 순서를 보장하려면 Task 완료 시 다음 Task를 큐에 등록하면 됩니다.
코드설명
class Main {
public static void main(String[] args){
var pool = Executors.newFixedThreadPool(2);
pool.submit(new Callback(new Continuation(3, "unlucky"), pool));
}
static class Callback implements Runnable{
Continuation cont;
ExecutorService pool;
Callback(Continuation cont, ExecutorService pool) {
this.cont = cont;
this.pool = pool;
}
public void run() {
cont = luck(cont);
System.out.println("my luck :" + cont.ret + " at " + Thread.currentThread().getName());
if(!Objects.equals(cont.ret, "lucky")){
pool.submit(this);
}
}
}
}
- 쓰레드 풀 생성 (Executors.newFixedThreadPool(2)) → 두 개의 쓰레드를 사용합니다.
- Callback 객체를 만들어 Task로 실행 합니다.
- luck() 함수를 호출하여 Continuation 상태를 업데이트 합니다.
- 결과가 "lucky"가 아니면 다시 Task를 큐에 등록하여 실행 반복 됩니다.
- Task 실행이 여러 쓰레드에서 처리될 수 있습니다.
정리
-
ForkJoinPool vs WorkerThreadPool
- ForkJoinPool은 개별 큐 + LIFO 구조, WorkerThreadPool은 공유 큐 + FIFO 구조 입니다.
- ForkJoinPool은 유연성이 부족하고 특정 프로젝트에서만 사용됩니다.
-
반제어 동시성 모델
- 콜백을 제어할 수 있는 최소한의 권한을 가지는 방식입니다.
- CompletableFuture는 Task 실행과 callback 호출을 분리 가능 입니다.
-
CPS (Continuation Passing Style)
- 함수 상태를 저장하고 다음 호출에 활용하는 패턴입니다.
- 함수 실행 상태를 관리하여 코루틴과 유사한 동작 구현 가능 입니다.
-
CPS + 워커쓰레드 패턴
- Task 실행을 함수 호출 방식으로 구성입니다.
- 실행 상태를 유지하면서 비동기 Task를 자연스럽게 연결 가능 입니다.