원문: https://davidvlijmincx.com/posts/loom/invoke-all-with-virtual-threads/ (Translated by Google Gemini)


서론#

invokeAllExecutorService 의 메서드로, 여러 제출된 작업을 동시에 시작합니다. ExecutorService 는 스레드 풀에서 플랫폼 스레드를 사용하여 제출된 작업을 실행합니다. 이 비싸고 리소스 집약적인 플랫폼 스레드를 사용하는 대신, 가상 스레드를 사용하여 ExecutorService 에 제출된 작업을 실행할 수도 있습니다. 이 글에서는 invokeAll 메서드를 가상 스레드 (virtual threads), 구조화된 동시성 (structured concurrency) 및 플랫폼 스레드 (platform threads) 와 함께 구현하는 모든 방법을 다룰 것입니다.

가상 스레드를 사용한 invokeAll#

우리가 살펴볼 첫 번째 예제는 가상 스레드 (virtual threads) 를 사용합니다. 각 작업에 대해 가상 스레드를 생성하는 Executor 를 생성하는 try-with-resources 문이 있습니다. 7행에서 invokeAll 메서드가 작업 목록과 함께 호출됩니다.

모든 작업을 시작한 후 결과를 기다리고 반환하는 스트림을 생성합니다.

static List<String> invokeAllWithVirtualThreads() throws ExecutionException, InterruptedException {
    try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
        var tasks = new ArrayList<Callable<String>>();
        tasks.add(() -> getStringFromResourceA());
        tasks.add(() -> getStringFromResourceB());

        List<Future<String>> futures = executor.invokeAll(tasks);

        return futures.stream().map(f -> {
            try {
                return f.get();
            } catch (InterruptedException | ExecutionException e) {
                throw new RuntimeRuntimeException(e);
            }
        }).collect(Collectors.toList());
    }
}

구조화된 동시성을 사용한 invokeAll#

구조화된 동시성 (structured concurrency) 은 Java 에서 스레드의 수명을 관리하는 새로운 방법입니다. 수명은 StructuredTaskScope 를 통해 제어됩니다. 주로 invokeAny 동작과 invokeAll 동작을 가진 두 가지 Scope 가 있습니다. 이 글에서는 invokeAll 을 구현할 것입니다. 모든 스레드가 실행을 마칠 때까지 기다리는 StructuredTaskScope 를 만들려면 ShutdownOnFailure 메서드를 호출해야 합니다.

Scope 가 실행할 작업을 추가하려면 fork 메서드를 호출해야 합니다. 7행의 scope.join() 메서드 호출은 모든 스레드가 완료될 때까지 차단됩니다. 10행에서 결과를 얻기 전에 예외가 발생했는지 확인합니다.

Scope 내부에 있기 때문에 get 을 호출하는 대신 새로운 resultNow 메서드를 사용하여 Future 에서 결과를 얻을 것입니다. 이는 스레드가 이미 완료될 때까지 기다렸기 때문에 새로운 선호되는 방법입니다.

static String invokeAllWithStructuredConcurrency() throws ExecutionException, InterruptedException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Future<String> futureA = scope.fork(() -> getStringFromResourceA());
        Future<String> futureB = scope.fork(() -> getStringFromResourceB());

        /* wait till all threads are done */
        scope.join();

        /* throw an exception if one occurred */
        scope.throwIfFailed();

        return "result: " + futureA.resultNow() + " " + futureB.resultNow();
    }
}

플랫폼 스레드를 사용한 invokeAll#

플랫폼 스레드 (platform threads) 는 이전 Java 버전에서 우리가 알고 있는 스레드입니다. 이 스레드는 운영 체제에서 관리되는 스레드와 밀접하게 연결되어 있습니다. 아래 예제는 ExecutorService 를 사용하기 때문에 가상 스레드를 사용하는 첫 번째 예제와 유사합니다. 이번에 사용하는 ExecutorServiceCachedThreadPool 입니다. 이 풀은 필요할 때 새 스레드를 생성하지만 이미 작업을 마친 스레드를 재사용합니다.

static List<String> invokeAllWithPlatformThreads() throws ExecutionException, InterruptedException {
    try (ExecutorService executor = Executors.newCachedThreadPool()) {
        var tasks = new ArrayList<Callable<String>>();
        tasks.add(() -> getStringFromResourceA());
        tasks.add(() -> getStringFromResourceB());

        List<Future<String>> futures = executor.invokeAll(tasks);

        return futures.stream().map(f -> {
            try {
                return f.get();
                } catch (InterruptedException | ExecutionException e) {
                    throw new RuntimeRuntimeException(e);
                }
            }).collect(Collectors.toList());
        }
    }

결론#

이 글에서는 invokeAll 메서드를 구현하는 세 가지 방법을 살펴보았습니다. 먼저 각 작업을 수행하는 가상 스레드를 생성하는 ExecutorService 를 사용했습니다. 그 다음에는 invokeAll 메서드처럼 동작하는 StructuredTaskScope 를 만드는 방법을 보았습니다. 마지막 예제는 이전 Java 버전에서 이미 익숙한 플랫폼 스레드를 사용했습니다.


더 읽어보기#

Java의 가상 스레드에 대한 추가 정보: