ExecutorService invokeAll 과 가상 스레드 사용하기
원문: https://davidvlijmincx.com/posts/loom/invoke-all-with-virtual-threads/ (Translated by Google Gemini)
서론#
invokeAll
은 ExecutorService
의 메서드로, 여러 제출된 작업을 동시에 시작합니다. 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
를 사용하기 때문에 가상 스레드를 사용하는 첫 번째 예제와 유사합니다. 이번에 사용하는 ExecutorService
는 CachedThreadPool
입니다. 이 풀은 필요할 때 새 스레드를 생성하지만 이미 작업을 마친 스레드를 재사용합니다.
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의 가상 스레드에 대한 추가 정보: