ForkJoinPool vs WorkerThreadPool - 차이점과 반제어 동시성 이해하기

궁금하면 500원·2025년 3월 11일

미생의 스프링

목록 보기
36/48

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 + 워커쓰레드 패턴 결합

원리

  1. CPS를 활용하여 함수 호출을 Task로 변환합니다.
  2. 각 함수 호출이 별도의 Task로 실행되며, 이전 상태(Continuation)를 다음 Task에 전달합니다.
  3. 매번 다른 쓰레드에서 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);
           }
       }
   }
}
  1. 쓰레드 풀 생성 (Executors.newFixedThreadPool(2)) → 두 개의 쓰레드를 사용합니다.
  2. Callback 객체를 만들어 Task로 실행 합니다.
  3. luck() 함수를 호출하여 Continuation 상태를 업데이트 합니다.
  4. 결과가 "lucky"가 아니면 다시 Task를 큐에 등록하여 실행 반복 됩니다.
  5. Task 실행이 여러 쓰레드에서 처리될 수 있습니다.

정리

  • ForkJoinPool vs WorkerThreadPool

    • ForkJoinPool은 개별 큐 + LIFO 구조, WorkerThreadPool은 공유 큐 + FIFO 구조 입니다.
    • ForkJoinPool은 유연성이 부족하고 특정 프로젝트에서만 사용됩니다.
  • 반제어 동시성 모델

    • 콜백을 제어할 수 있는 최소한의 권한을 가지는 방식입니다.
    • CompletableFuture는 Task 실행과 callback 호출을 분리 가능 입니다.
  • CPS (Continuation Passing Style)

    • 함수 상태를 저장하고 다음 호출에 활용하는 패턴입니다.
    • 함수 실행 상태를 관리하여 코루틴과 유사한 동작 구현 가능 입니다.
  • CPS + 워커쓰레드 패턴

    • Task 실행을 함수 호출 방식으로 구성입니다.
    • 실행 상태를 유지하면서 비동기 Task를 자연스럽게 연결 가능 입니다.
profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글