본문 바로가기

Future.get과 병렬 합산

제출 직후 get이 후속 제출을 늦추는 흐름을 분석하고, 작업을 먼저 제출한 뒤 수집 시작 시점의 공통 예산으로 결과를 모읍니다.

Future를 사용한다고 자동으로 병렬 실행이 되는 것은 아닙니다.

작업 하나를 제출하고 즉시 get한 뒤 다음 작업을 제출하면 호출자가 첫 완료를 기다리는 동안 두 번째 계산은 시작조차 하지 않습니다.

독립 작업을 먼저 제출하고 나중에 결과를 수집하면 실행 구간이 겹칠 수 있습니다.

get이 블로킹하는 위치도 응답 설계의 일부입니다.

메인 요청 스레드에서 무제한 get을 호출하면 느린 작업 하나가 전체 응답을 붙잡습니다.

공통 마감 시간을 전달하거나 비동기로 결과를 연결하고, 시간 초과 뒤의 취소와 종료 정책도 정합니다.


submit·get 반복에 따른 직렬화

bad/SequentialFutureWait.java
import java.util.concurrent.Executors;

public final class SequentialFutureWait {
    public static void main(String[] args) throws Exception {
        var executor = Executors.newFixedThreadPool(2);
        long start = System.nanoTime();
        try {
            int first = executor.submit(() -> {
                Thread.sleep(200);
                return 20;
            }).get();
            int second = executor.submit(() -> {
                Thread.sleep(200);
                return 22;
            }).get();
            System.out.println("sum=" + (first + second));
            System.out.println("millis=" + (System.nanoTime() - start) / 1_000_000);
        } finally {
            executor.shutdown();
        }
    }
}

한 번의 실행에서는 sum=42, millis=409를 출력했습니다. 두 번째 제출이 첫 get의 반환 뒤에 있으므로 두 sleep 작업은 차례로 실행됩니다. millis에는 제출·수집·출력 비용과 스케줄링이 포함됩니다.

아래 ParallelRangeSum은 sleep 없이 작은 정수 범위를 합산합니다. 이 절에는 같은 sleep 작업을 병렬로 실행한 400ms 대 200ms 비교 실험이 없습니다.


병렬 분할의 검토 항목

  • 서로 독립적인 작업만 동시에 제출한다.
  • 제출 단계에서 모든 Future 참조를 확보한다.
  • 결과 수집 순서와 업무 결과 순서를 별도로 결정한다.
  • 분할 비용이 계산 이득보다 작은지 측정한다.
  • 한 작업 실패가 전체 실패인지 부분 결과 허용인지 정한다.
  • 정한 수집 마감 시간에서 남은 시간을 각 get에 전달한다.

병렬 범위 합산

src/ParallelRangeSum.java
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public final class ParallelRangeSum {
    static long sum(long start, long endInclusive) {
        long result = 0;
        for (long value = start; value <= endInclusive; value++) {
            result += value;
        }
        return result;
    }

    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        try {
            List<Future<Long>> futures = List.of(
                    executor.submit(() -> sum(1, 50)),
                    executor.submit(() -> sum(51, 100)));
            long total = 0;
            for (Future<Long> future : futures) {
                total += future.get();
            }
            System.out.println("total=" + total);
        } finally {
            executor.shutdown();
        }
    }
}

첫 get 이전에 제출된 작업 수를 비교한다

submit/get을 교차하는 순서와 두 submit 뒤 get을 하는 순서가 실제 코드 호출 순서입니다. 고정된 두 작업의 제출·대기 경계를 따라가며 제출된 작업 수를 확인할 수 있습니다.

첫 get 이전에 제출된 작업 수를 비교한다

SequentialFutureWait의 두 sleep 작업과 ParallelRangeSum의 두 합산 작업을 코드 순서 관점에서 비교합니다. 같은 부하의 시간 비교가 아닙니다.

제출 순서 · 첫 get 이전

첫 get 이전에 제출된 작업 수를 비교한다

두 작업을 모두 제출한 뒤 get하면 둘 다 먼저 실행할 수 있습니다.

첫 get 이전의 두 제출 경계를 비교한다화살표는 호출자 코드의 다음 호출 순서다. 위 프로그램은 첫 get 반환 뒤 둘째를 제출하고 아래 프로그램은 둘 다 제출한 뒤 첫 get을 호출한다. 두 프로그램은 서로 다른 작업이므로 같은 부하의 시간 비교나 작업자 실제 실행 순서를 나타내지 않는다.첫 submit첫 get 반환둘째 submit둘째 get 반환1..50 제출51..100 제출첫 get둘째 getSequentialFutureWait · 코드 호출 순서ParallelRangeSum · 코드 호출 순서아래 흐름에서는 첫 get 전에 두 작업을 제출함
첫 get 이전의 두 제출 경계를 비교한다화살표는 호출자 코드의 다음 호출 순서다. 위 프로그램은 첫 get 반환 뒤 둘째를 제출하고 아래 프로그램은 둘 다 제출한 뒤 첫 get을 호출한다. 두 프로그램은 서로 다른 작업이므로 같은 부하의 시간 비교나 작업자 실제 실행 순서를 나타내지 않는다.첫 submit첫 get 반환둘째 submit둘째 get 반환1..50 제출51..100 제출첫 get둘째 getSequentialFutureWait첫 반환 뒤 둘째 제출ParallelRangeSum첫 get 전에 두 제출

SequentialFutureWait

첫 get 전에 제출한 작업
첫 sleep 작업 하나
다음 작업이 시작할 수 있는 경계
첫 결과를 받은 다음에야 두 번째 sleep 작업을 제출

ParallelRangeSum

첫 get 전에 제출한 작업
1..50 합산과 51..100 합산 모두
다음 작업이 시작할 수 있는 경계
첫 결과 수집 전에 둘 다 실행할 수 있음

둘 다 미리 제출하면 실행이 겹칠 수 있지만, 작은 작업의 실제 겹침을 측정한 것은 아닙니다. 두 프로그램의 작업 내용이 다르므로 출력 시간을 같은 부하의 속도 비교로 쓰지 않습니다.

이 예의 조건

  • 서로 다른 작업의 프로그램이며 실행 시간이나 worker 순서 비교가 아닙니다.
관계도의 보충 설명
원 도표의 설명과 주석
구분내용
전체 읽기 정보첫 get 이전에 제출된 작업 수를 비교한다 첫 get 이전에 제출된 작업 수를 비교한다 SequentialFutureWait의 두 sleep 작업과 ParallelRangeSum의 두 합산 작업을 코드 순서 관점에서 비교합니다. 같은 부하의 시간 비교가 아닙니다. 제출 순서 · 첫 get 이전 첫 get 이전에 제출된 작업 수를 비교한다 두 작업을 모두 제출한 뒤 get하면 둘 다 먼저 실행할 수 있습니다. SequentialFutureWait 첫 get 전에 제출한 작업 첫 sleep 작업 하나 다음 작업이 시작할 수 있는 경계 첫 결과를 받은 다음에야 두 번째 sleep 작업을 제출 ParallelRangeSum 첫 get 전에 제출한 작업 1..50 합산과 51..100 합산 모두 다음 작업이 시작할 수 있는 경계 첫 결과 수집 전에 둘 다 실행할 수 있음 둘 다 미리 제출하면 실행이 겹칠 수 있지만, 작은 작업의 실제 겹침을 측정한 것은 아닙니다. 두 프로그램의 작업 내용이 다르므로 출력 시간을 같은 부하의 속도 비교로 쓰지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 제출

Sequential은 첫 작업만, Parallel은 두 범위를 제출합니다.

  • Sequential: 첫 submit
  • Parallel: 1..50과 51..100 submit
  • 첫 get 전 제출된 작업 수가 다릅니다.
첫 대기 경계

Sequential은 첫 get 반환 뒤 둘째를 제출합니다.

  • Sequential: 첫 get→둘째 submit
  • Parallel: 두 submit→첫 get
  • 호출자 제출 순서가 겹친 실행 가능 범위를 바꿉니다.
결과 회수

각 프로그램이 둘째 get까지 진행합니다.

  • 표의 의미: 호출자 호출 순서
  • 의미 제외: 동일 부하 시간 비교
  • 제출 순서에서 실제 worker 실행 순서를 추론하지 않습니다.

첫 get 이전에 제출된 작업 수를 비교한다

submit/get을 교차하는 순서와 두 submit 뒤 get을 하는 순서가 실제 코드 호출 순서입니다. 고정된 두 작업의 제출·대기 경계를 따라가며 제출된 작업 수를 확인할 수 있습니다.

첫 get 이전에 제출된 작업 수를 비교한다

SequentialFutureWait의 두 sleep 작업과 ParallelRangeSum의 두 합산 작업을 코드 순서 관점에서 비교합니다. 같은 부하의 시간 비교가 아닙니다.

제출 순서 · 첫 get 이전

첫 get 이전에 제출된 작업 수를 비교한다

두 작업을 모두 제출한 뒤 get하면 둘 다 먼저 실행할 수 있습니다.

첫 get 이전의 두 제출 경계를 비교한다화살표는 호출자 코드의 다음 호출 순서다. 위 프로그램은 첫 get 반환 뒤 둘째를 제출하고 아래 프로그램은 둘 다 제출한 뒤 첫 get을 호출한다. 두 프로그램은 서로 다른 작업이므로 같은 부하의 시간 비교나 작업자 실제 실행 순서를 나타내지 않는다.첫 submit첫 get 반환둘째 submit둘째 get 반환1..50 제출51..100 제출첫 get둘째 getSequentialFutureWait · 코드 호출 순서ParallelRangeSum · 코드 호출 순서아래 흐름에서는 첫 get 전에 두 작업을 제출함
첫 get 이전의 두 제출 경계를 비교한다화살표는 호출자 코드의 다음 호출 순서다. 위 프로그램은 첫 get 반환 뒤 둘째를 제출하고 아래 프로그램은 둘 다 제출한 뒤 첫 get을 호출한다. 두 프로그램은 서로 다른 작업이므로 같은 부하의 시간 비교나 작업자 실제 실행 순서를 나타내지 않는다.첫 submit첫 get 반환둘째 submit둘째 get 반환1..50 제출51..100 제출첫 get둘째 getSequentialFutureWait첫 반환 뒤 둘째 제출ParallelRangeSum첫 get 전에 두 제출

SequentialFutureWait

첫 get 전에 제출한 작업
첫 sleep 작업 하나
다음 작업이 시작할 수 있는 경계
첫 결과를 받은 다음에야 두 번째 sleep 작업을 제출

ParallelRangeSum

첫 get 전에 제출한 작업
1..50 합산과 51..100 합산 모두
다음 작업이 시작할 수 있는 경계
첫 결과 수집 전에 둘 다 실행할 수 있음

둘 다 미리 제출하면 실행이 겹칠 수 있지만, 작은 작업의 실제 겹침을 측정한 것은 아닙니다. 두 프로그램의 작업 내용이 다르므로 출력 시간을 같은 부하의 속도 비교로 쓰지 않습니다.

이 예의 조건

  • 서로 다른 작업의 프로그램이며 실행 시간이나 worker 순서 비교가 아닙니다.

관계도의 보충 설명

원 도표의 설명과 주석
구분내용
전체 읽기 정보첫 get 이전에 제출된 작업 수를 비교한다 첫 get 이전에 제출된 작업 수를 비교한다 SequentialFutureWait의 두 sleep 작업과 ParallelRangeSum의 두 합산 작업을 코드 순서 관점에서 비교합니다. 같은 부하의 시간 비교가 아닙니다. 제출 순서 · 첫 get 이전 첫 get 이전에 제출된 작업 수를 비교한다 두 작업을 모두 제출한 뒤 get하면 둘 다 먼저 실행할 수 있습니다. SequentialFutureWait 첫 get 전에 제출한 작업 첫 sleep 작업 하나 다음 작업이 시작할 수 있는 경계 첫 결과를 받은 다음에야 두 번째 sleep 작업을 제출 ParallelRangeSum 첫 get 전에 제출한 작업 1..50 합산과 51..100 합산 모두 다음 작업이 시작할 수 있는 경계 첫 결과 수집 전에 둘 다 실행할 수 있음 둘 다 미리 제출하면 실행이 겹칠 수 있지만, 작은 작업의 실제 겹침을 측정한 것은 아닙니다. 두 프로그램의 작업 내용이 다르므로 출력 시간을 같은 부하의 속도 비교로 쓰지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 제출

Sequential은 첫 작업만, Parallel은 두 범위를 제출합니다.

  • Sequential: 첫 submit
  • Parallel: 1..50과 51..100 submit
  • 첫 get 전 제출된 작업 수가 다릅니다.
첫 대기 경계

Sequential은 첫 get 반환 뒤 둘째를 제출합니다.

  • Sequential: 첫 get→둘째 submit
  • Parallel: 두 submit→첫 get
  • 호출자 제출 순서가 겹친 실행 가능 범위를 바꿉니다.
결과 회수

각 프로그램이 둘째 get까지 진행합니다.

  • 표의 의미: 호출자 호출 순서
  • 의미 제외: 동일 부하 시간 비교
  • 제출 순서에서 실제 worker 실행 순서를 추론하지 않습니다.

결과는 5,050입니다.

왼쪽은 1 + … + 50 = 50 × 51 ÷ 2 = 1,275이고, 오른쪽은 51 + … + 100 = (51 + 100) × 50 ÷ 2 = 3,775입니다. 합이 5,050이며 50과 51 사이에 누락·중복이 없습니다. 예컨대 두 번째 범위를 50부터 시작하면 50을 두 번 더해 5,100이 되므로 실행기 선택과 별개로 분할 경계를 먼저 검산해야 합니다.

범위가 겹치거나 빠지지 않는지 분할 경계도 테스트해야 합니다.

이 작은 합산은 구조 설명용입니다. 병렬 준비 비용과 계산 이득은 실제 입력 크기와 부하에서 비교해야 합니다.

이 반복문의 입력 범위도 계약입니다. 예제는 1..100처럼 작고 양수인 범위를 사용합니다. 끝값이 Long.MAX_VALUE이면 마지막 value++가 최솟값으로 넘어가 종료 조건을 다시 만족할 수 있으므로 같은 반복문을 모든 long 범위에 그대로 확대하지 않습니다. 부분합과 전체합이 long 안에 드는지도 별도로 확인해야 합니다. 더 넓은 범위로 확장할 때는 반복 종료 방식과 덧셈 오버플로 처리부터 정합니다.


하나의 기한으로 여러 결과 기다리기

기한의 시작점과 반환값 조건을 먼저 정합니다. 아래 collect의 예산은 메서드 진입 때 시작하므로, 그 전에 작업을 제출하는 데 쓴 시간은 빠집니다. 요청 전체를 같은 기한으로 제한하려면 요청 시작 때 만든 기한에서 남은 시간을 전달하는 별도 설계가 필요합니다. 이 예제에는 나노초로 표현할 수 있는 짧은 양수 예산과 null이 아닌 결과를 전달한다고 가정합니다. Future가 정상적으로 null을 반환할 수는 있지만 마지막 List.copyOf는 null 원소를 거부합니다. 이때도 catch에서 취소를 시도하고 예외를 다시 던지므로, 앞서 얻은 값의 부분 목록을 반환하지 않습니다.

src/DeadlineFutureCollector.java
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public final class DeadlineFutureCollector {
    static <T> List<T> collect(List<Future<T>> futures, Duration budget) throws Exception {
        long deadline = System.nanoTime() + budget.toNanos();
        List<T> results = new ArrayList<>();
        try {
            for (Future<T> future : futures) {
                long remaining = deadline - System.nanoTime();
                if (remaining <= 0) throw new TimeoutException("batch deadline");
                results.add(future.get(remaining, TimeUnit.NANOSECONDS));
            }
            return List.copyOf(results);
        } catch (Exception e) {
            futures.forEach(future -> future.cancel(true));
            throw e;
        }
    }

    public static void main(String[] args) throws Exception {
        var executor = java.util.concurrent.Executors.newFixedThreadPool(2);
        try {
            var futures = List.of(executor.submit(() -> 7), executor.submit(() -> 8));
            System.out.println(collect(futures, Duration.ofSeconds(1)));
        } finally {
            executor.shutdownNow();
        }
    }
}

잔여 예산 검사와 취소 요청의 경계를 구분한다

하나의 기한에서 각 Future get의 정상값을 누적하고 실패 시 모두 cancel·재전파합니다. 현재 Future·부분 목록·실패 후 미반환을 추적합니다.

잔여 예산 검사와 취소 요청의 경계를 구분한다

DeadlineFutureCollector.collect의 기한 검사, 결과 추가, 목록 반환, 예외 후 취소 시도와 재전파를 보여줍니다.

배치 수집 · 잔여 예산과 실패 처리

잔여 예산 검사와 취소 요청의 경계를 구분한다

collect에서 시작한 한 예산을 get마다 계산하고, 실패하면 취소를 시도한 뒤 예외를 전파합니다.

한 기한을 쓰는 결과 수집과 예외 경계를 연결한다기한은collect진입에서 시작한다. 다음Future가있고 remaining이양수이면 기한get의정상값을추가한뒤반복한다.항목이없으면List.copyOf를반환한다.remaining<=0, get예외또는copy예외는모든Future취소시도와재전파로향한다.그림의실선은코드제어흐름이며 취소성공/작업자종료보장이 아니다.예예다음 반복없음아니요: 시간 초과get·copy 예외도 catchcollect 진입한 기한 계산다음 Future?더 있음remaining > 0?기한 get정상값을 추가목록 복사 반환List.copyOfException 처리모두 cancel(true)예외 재전파catch는 부분 목록을 반환하지 않음
한 기한을 쓰는 결과 수집과 예외 경계를 연결한다기한은collect진입에서 시작한다. 다음Future가있고 remaining이양수이면 기한get의정상값을추가한뒤반복한다.항목이없으면List.copyOf를반환한다.remaining<=0, get예외또는copy예외는모든Future취소시도와재전파로향한다.그림의실선은코드제어흐름이며 취소성공/작업자종료보장이 아니다.예예copy 예외: 왼쪽 선collect 진입한 기한 계산다음 Future?더 있음remaining > 0?기한 get정상값을 추가목록 복사 반환List.copyOfException 처리모두 cancel(true)예외 재전파왼쪽 선: 정상값 후 반복오른쪽 바깥: 항목 없음목록 복사 성공 때 반환오른쪽: 시간 초과·예외
  1. remaining ≤ 0

    원문이 하는 일
    TimeoutException("batch deadline") 발생
    반환·취소의 경계
    이미 완료한 Future라도 이 검사에서 중단 가능
  2. future.get(remaining, NANOSECONDS)

    원문이 하는 일
    정상 결과를 results에 추가
    반환·취소의 경계
    아직 호출자에게 부분 목록을 반환하지 않음
  3. 모든 get 뒤 List.copyOf(results)

    원문이 하는 일
    결과 목록의 복사본 반환
    반환·취소의 경계
    원소 객체를 깊게 복사하지 않으며 null 원소는 거부
  4. Exception을 잡은 catch

    원문이 하는 일
    모든 Future에 cancel(true) 시도 후 예외 재전파
    반환·취소의 경계
    취소 성공 여부를 검사하지 않고 부분 결과도 반환하지 않음

한 번의 실행에서는 [7, 8]을 반환했으며 시간 초과·취소 경로는 관측하지 않았습니다. 예산은 collect 진입 때 시작해 앞선 제출 시간은 제외합니다. 잔여 get 예산을 재사용해도 스케줄링·취소·실행기 종료를 포함한 전체 반환 시각의 상한은 보장하지 않습니다.

이 예의 조건

  • collect 진입의 한 기한을 공유하고 catch는 부분 목록을 반환하지 않습니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2기한은 collect 진입에서 시작한다. 다음 Future가 있고 remaining이 양수이면 기한 get의 정상값을 추가한 뒤 반복한다. 항목이 없으면 List.copyOf를 반환한다. remaining<=0, get 예외 또는 copy 예외는 모든 Future의 취소 시도와 재전파로 향한다. 그림의 실선은 코드 제어 흐름이며 취소 성공/작업자 종료 보장이 아니다.
원 도표의 설명과 주석
구분내용
설명기한은 collect 진입에서 시작한다. 다음 Future가 있고 remaining이 양수이면 기한 get의 정상값을 추가한 뒤 반복한다. 항목이 없으면 List.copyOf를 반환한다. remaining<=0, get 예외 또는 copy 예외는 모든 Future의 취소 시도와 재전파로 향한다. 그림의 실선은 코드 제어 흐름이며 취소 성공/작업자 종료 보장이 아니다.
전체 읽기 정보잔여 예산 검사와 취소 요청의 경계를 구분한다 잔여 예산 검사와 취소 요청의 경계를 구분한다 DeadlineFutureCollector.collect의 기한 검사, 결과 추가, 목록 반환, 예외 후 취소 시도와 재전파를 보여줍니다. 배치 수집 · 잔여 예산과 실패 처리 잔여 예산 검사와 취소 요청의 경계를 구분한다 collect에서 시작한 한 예산을 get마다 계산하고, 실패하면 취소를 시도한 뒤 예외를 전파합니다. remaining ≤ 0 원문이 하는 일 TimeoutException("batch deadline") 발생 반환·취소의 경계 이미 완료한 Future라도 이 검사에서 중단 가능 future.get(remaining, NANOSECONDS) 원문이 하는 일 정상 결과를 results에 추가 반환·취소의 경계 아직 호출자에게 부분 목록을 반환하지 않음 모든 get 뒤 List.copyOf(results) 원문이 하는 일 결과 목록의 복사본 반환 반환·취소의 경계 원소 객체를 깊게 복사하지 않으며 null 원소는 거부 Exception을 잡은 catch 원문이 하는 일 모든 Future에 cancel(true) 시도 후 예외 재전파 반환·취소의 경계 취소 성공 여부를 검사하지 않고 부분 결과도 반환하지 않음 한 번의 실행에서는 [7, 8]을 반환했으며 시간 초과·취소 경로는 관측하지 않았습니다. 예산은 collect 진입 때 시작해 앞선 제출 시간은 제외합니다. 잔여 get 예산을 재사용해도 스케줄링·취소·실행기 종료를 포함한 전체 반환 시각의 상한은 보장하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
수집 시작

기한을 한 번 계산하고 다음 Future 여부를 봅니다.

  • 다음 없음: 목록 복사 반환
  • 다음 있음: remaining 계산
  • 기한은 각 get마다 새로 시작하지 않습니다.
정상값 누적

remaining>0일 때 기한 get을 호출하고 값을 추가합니다.

  • 목록: 완료한 정상값 누적
  • 다음: 반복
  • 반환 목록은 모든 수집과 복사가 정상 완료해야 나옵니다.
예외 처리

remaining≤0 또는 get·copy 예외가 catch로 갑니다.

  • 동작: 모두 cancel(true) 시도
  • 결과: 예외 재전파
  • 취소 요청은 취소 성공·worker 종료 보장이 아닙니다.

잔여 예산 검사와 취소 요청의 경계를 구분한다

하나의 기한에서 각 Future get의 정상값을 누적하고 실패 시 모두 cancel·재전파합니다. 현재 Future·부분 목록·실패 후 미반환을 추적합니다.

잔여 예산 검사와 취소 요청의 경계를 구분한다

DeadlineFutureCollector.collect의 기한 검사, 결과 추가, 목록 반환, 예외 후 취소 시도와 재전파를 보여줍니다.

배치 수집 · 잔여 예산과 실패 처리

잔여 예산 검사와 취소 요청의 경계를 구분한다

collect에서 시작한 한 예산을 get마다 계산하고, 실패하면 취소를 시도한 뒤 예외를 전파합니다.

한 기한을 쓰는 결과 수집과 예외 경계를 연결한다기한은collect진입에서 시작한다. 다음Future가있고 remaining이양수이면 기한get의정상값을추가한뒤반복한다.항목이없으면List.copyOf를반환한다.remaining<=0, get예외또는copy예외는모든Future취소시도와재전파로향한다.그림의실선은코드제어흐름이며 취소성공/작업자종료보장이 아니다.예예다음 반복없음아니요: 시간 초과get·copy 예외도 catchcollect 진입한 기한 계산다음 Future?더 있음remaining > 0?기한 get정상값을 추가목록 복사 반환List.copyOfException 처리모두 cancel(true)예외 재전파catch는 부분 목록을 반환하지 않음
한 기한을 쓰는 결과 수집과 예외 경계를 연결한다기한은collect진입에서 시작한다. 다음Future가있고 remaining이양수이면 기한get의정상값을추가한뒤반복한다.항목이없으면List.copyOf를반환한다.remaining<=0, get예외또는copy예외는모든Future취소시도와재전파로향한다.그림의실선은코드제어흐름이며 취소성공/작업자종료보장이 아니다.예예copy 예외: 왼쪽 선collect 진입한 기한 계산다음 Future?더 있음remaining > 0?기한 get정상값을 추가목록 복사 반환List.copyOfException 처리모두 cancel(true)예외 재전파왼쪽 선: 정상값 후 반복오른쪽 바깥: 항목 없음목록 복사 성공 때 반환오른쪽: 시간 초과·예외
  1. remaining ≤ 0

    원문이 하는 일
    TimeoutException("batch deadline") 발생
    반환·취소의 경계
    이미 완료한 Future라도 이 검사에서 중단 가능
  2. future.get(remaining, NANOSECONDS)

    원문이 하는 일
    정상 결과를 results에 추가
    반환·취소의 경계
    아직 호출자에게 부분 목록을 반환하지 않음
  3. 모든 get 뒤 List.copyOf(results)

    원문이 하는 일
    결과 목록의 복사본 반환
    반환·취소의 경계
    원소 객체를 깊게 복사하지 않으며 null 원소는 거부
  4. Exception을 잡은 catch

    원문이 하는 일
    모든 Future에 cancel(true) 시도 후 예외 재전파
    반환·취소의 경계
    취소 성공 여부를 검사하지 않고 부분 결과도 반환하지 않음

한 번의 실행에서는 [7, 8]을 반환했으며 시간 초과·취소 경로는 관측하지 않았습니다. 예산은 collect 진입 때 시작해 앞선 제출 시간은 제외합니다. 잔여 get 예산을 재사용해도 스케줄링·취소·실행기 종료를 포함한 전체 반환 시각의 상한은 보장하지 않습니다.

이 예의 조건

  • collect 진입의 한 기한을 공유하고 catch는 부분 목록을 반환하지 않습니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2기한은 collect 진입에서 시작한다. 다음 Future가 있고 remaining이 양수이면 기한 get의 정상값을 추가한 뒤 반복한다. 항목이 없으면 List.copyOf를 반환한다. remaining<=0, get 예외 또는 copy 예외는 모든 Future의 취소 시도와 재전파로 향한다. 그림의 실선은 코드 제어 흐름이며 취소 성공/작업자 종료 보장이 아니다.
원 도표의 설명과 주석
구분내용
설명기한은 collect 진입에서 시작한다. 다음 Future가 있고 remaining이 양수이면 기한 get의 정상값을 추가한 뒤 반복한다. 항목이 없으면 List.copyOf를 반환한다. remaining<=0, get 예외 또는 copy 예외는 모든 Future의 취소 시도와 재전파로 향한다. 그림의 실선은 코드 제어 흐름이며 취소 성공/작업자 종료 보장이 아니다.
전체 읽기 정보잔여 예산 검사와 취소 요청의 경계를 구분한다 잔여 예산 검사와 취소 요청의 경계를 구분한다 DeadlineFutureCollector.collect의 기한 검사, 결과 추가, 목록 반환, 예외 후 취소 시도와 재전파를 보여줍니다. 배치 수집 · 잔여 예산과 실패 처리 잔여 예산 검사와 취소 요청의 경계를 구분한다 collect에서 시작한 한 예산을 get마다 계산하고, 실패하면 취소를 시도한 뒤 예외를 전파합니다. remaining ≤ 0 원문이 하는 일 TimeoutException("batch deadline") 발생 반환·취소의 경계 이미 완료한 Future라도 이 검사에서 중단 가능 future.get(remaining, NANOSECONDS) 원문이 하는 일 정상 결과를 results에 추가 반환·취소의 경계 아직 호출자에게 부분 목록을 반환하지 않음 모든 get 뒤 List.copyOf(results) 원문이 하는 일 결과 목록의 복사본 반환 반환·취소의 경계 원소 객체를 깊게 복사하지 않으며 null 원소는 거부 Exception을 잡은 catch 원문이 하는 일 모든 Future에 cancel(true) 시도 후 예외 재전파 반환·취소의 경계 취소 성공 여부를 검사하지 않고 부분 결과도 반환하지 않음 한 번의 실행에서는 [7, 8]을 반환했으며 시간 초과·취소 경로는 관측하지 않았습니다. 예산은 collect 진입 때 시작해 앞선 제출 시간은 제외합니다. 잔여 get 예산을 재사용해도 스케줄링·취소·실행기 종료를 포함한 전체 반환 시각의 상한은 보장하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
수집 시작

기한을 한 번 계산하고 다음 Future 여부를 봅니다.

  • 다음 없음: 목록 복사 반환
  • 다음 있음: remaining 계산
  • 기한은 각 get마다 새로 시작하지 않습니다.
정상값 누적

remaining>0일 때 기한 get을 호출하고 값을 추가합니다.

  • 목록: 완료한 정상값 누적
  • 다음: 반복
  • 반환 목록은 모든 수집과 복사가 정상 완료해야 나옵니다.
예외 처리

remaining≤0 또는 get·copy 예외가 catch로 갑니다.

  • 동작: 모두 cancel(true) 시도
  • 결과: 예외 재전파
  • 취소 요청은 취소 성공·worker 종료 보장이 아닙니다.

예시

collect에 들어올 때 공통 예산이 100ms이고 첫 get과 그 사이 처리에 60ms를 썼다고 가정하세요. 다음 get에는 약 40ms가 남습니다. 두 번째 Future의 결과가 이 시점부터 50ms 뒤에 준비된다면 잔여 예산 안에 받지 못합니다. 매번 100ms씩 새로 기다리는 정책과 다릅니다. 이미 끝난 결과도 remaining ≤ 0 검사에 먼저 걸리면 이 코드는 받지 않고 시간 초과를 냅니다. 이 숫자는 원 main의 [7, 8] 실행을 측정한 값이 아닙니다.


결과 수집 방식 비교

결과 요구수집 방식특성
제출 순서 결과Future 목록 순회느린 앞 작업이 뒤 결과 지연
완료 순 결과CompletionService빠른 결과 먼저 처리
단계 조합CompletableFuture비동기 변환 연결
수집 대기 예산기한 get각 호출에 잔여 예산 전달

분할 수를 정하는 근거

작업 조각이 너무 크면 가장 느린 조각이 전체 완료를 늦추고, 너무 작으면 제출·큐잉·결과 결합 비용이 실제 계산보다 커집니다.

CPU 계산은 사용 가능한 프로세서 수를 출발점으로 삼고, 각 조각의 입력 분포가 비슷한지 측정합니다.

외부 I/O가 섞인다면 프로세서 수만으로 병렬도를 정하지 말고 대상 시스템의 연결 상한과 허용 요청률을 먼저 봅니다.

병렬 합산의 정확성도 성능과 함께 확인합니다.

int 범위를 넘을 수 있으면 부분합과 최종합에 long을 검토하되, long의 범위도 넘는 입력은 별도로 처리해야 합니다. 부동소수점은 결합 순서가 바뀌면 마지막 비트가 달라질 수 있음을 허용 오차에 반영합니다.

기준 순차 구현과 무작위 입력을 비교해 값이 맞는지 확인한 뒤에만 처리 시간을 비교하세요.

입력이 작을 때는 순차 경로를 선택하는 임계값도 둡니다.

측정에서 병렬 준비 비용이 계산 절감보다 큰 구간을 찾아 기준을 정하면 간단한 요청이 큐와 스레드를 불필요하게 거치지 않습니다.


연습 문제

1부터 1,000까지를 두 범위로 나눠 홀수와 짝수 개수를 각각 Callable로 계산하고, 두 작업을 모두 제출한 뒤 결과를 합치세요.

정답과 해설
exercise/ParallelParitySolution.java
import java.util.concurrent.Executors;

public final class ParallelParitySolution {
    record Counts(int odd, int even) {
        Counts plus(Counts other) { return new Counts(odd + other.odd, even + other.even); }
    }

    static Counts count(int start, int end) {
        int odd = 0, even = 0;
        for (int n = start; n <= end; n++) {
            if ((n & 1) == 0) {
                even++;
            } else {
                odd++;
            }
        }
        return new Counts(odd, even);
    }

    public static void main(String[] args) throws Exception {
        var executor = Executors.newFixedThreadPool(2);
        try {
            var left = executor.submit(() -> count(1, 500));
            var right = executor.submit(() -> count(501, 1_000));
            System.out.println(left.get().plus(right.get()));
        } finally {
            executor.shutdown();
        }
    }
}

결과는 odd 500, even 500입니다.

두 submit이 두 get보다 앞에 있어 두 계산이 겹칠 수 있습니다. 이 main은 각 계산의 시작·종료 시각을 기록하지 않습니다.

확인 문제

새 범위를 2부터 1,000까지로 두고 2..500과 501..1,000으로 나눠 보세요. 각 부분과 최종 Counts의 odd·even 값은 무엇이며, 전체 개수 999와도 맞나요?

확인 문제 해설

2..500은 odd=249, even=250이고 501..1,000은 odd=250, even=250입니다. 합은 odd=499, even=500이며 두 값을 합하면 999입니다. 원 문제의 1을 제외했으므로 홀수 하나가 줄어듭니다. 두 범위의 접점 500/501도 누락·중복 없이 이어집니다.


병렬 합산을 채택하는 기준

Future는 작업을 대표할 뿐 병렬 구조를 자동 구성하지 않습니다.

독립 작업을 먼저 모두 제출하고 수집 지점의 블로킹과 시간 상한을 명시해야 실제 병렬성과 응답 규칙이 함께 살아납니다.

범위와 수집 계약을 바꾸는 추가 연습

  1. ParallelParitySolution의 대상 범위를 3..999로 바꾸고, 3..500과 501..999를 각각 계산한다. 양 끝은 포함한다. 각 부분과 최종 홀수·짝수 개수, 전체 원소 수를 구하고 경계가 이어지는지 확인하라.
  2. 작업 제출 준비에 이미 30ms를 쓴 뒤 collect를 100ms 예산으로 시작한다. 첫 결과와 그 사이 처리에 60ms를 쓰고, 두 번째 결과는 그 시점에서 50ms 뒤에 준비된다. 시간 숫자는 정책 검산을 위한 가정이다. 두 번째 get에 새로 100ms를 주는가? 남은 대기 예산은 얼마이며 결과를 받을 수 있는가? 앞선 30ms는 이 collect 기한 안에 포함되는가?
  3. 양수 예산이 충분히 남아 있고 외부 취소·작업 예외가 없다. 이미 정상 완료한 두 Future의 결과가 순서대로 7과 null이다. collect는 [7, null]을 반환하는가? 실패 지점과 재전파되는 예외, 첫 값 7의 부분 반환 여부를 설명하라.
추가 연습 풀이
  1. 3..500은 홀수 249개·짝수 249개, 501..999는 홀수 250개·짝수 249개다. 합은 홀수 499개·짝수 498개이며 전체 997개 = 999 − 3 + 1이다. 첫 범위가 500에서 끝나고 다음 범위가 501에서 시작하므로 누락·중복이 없다.
  2. 다음 대기의 예산은 약 40ms = 100 − 60이며 새 100ms가 아니다. 50ms 뒤의 결과는 이 잔여 예산 안에 받지 못해 시간 초과 경로로 간다. 제출 준비 30ms는 collect 진입 전에 썼으므로 제외된다. 이 기한은 요청 시작부터의 총 100ms 상한이 아니며, 스케줄링·취소 처리·실행기 종료까지 포함한 실제 반환 시각을 숫자로 보장하지도 않는다.
  3. 두 get 자체는 정상 반환하여 내부 results에 7과 null이 들어간다. 마지막 List.copyOf(results)가 NullPointerException을 던지고, catch가 모든 Future의 취소를 시도한 뒤 같은 예외를 다시 던진다. 7만 든 부분 목록도 반환하지 않는다. 이미 완료한 Future에 취소를 요청한다고 완료 결과가 취소로 바뀌는 것은 아니다.