일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- Akka
- 파이썬 데이터분석
- 파이썬
- 엔터프라이즈 블록체인
- 스칼라 강좌
- Play2 로 웹 개발
- 주키퍼
- CORDA
- 그라파나
- play2 강좌
- 스칼라 동시성
- 블록체인
- 이더리움
- 스위프트
- akka 강좌
- Actor
- Adapter 패턴
- 하이브리드앱
- Golang
- hyperledger fabric
- 파이썬 강좌
- 플레이프레임워크
- play 강좌
- 파이썬 머신러닝
- Hyperledger fabric gossip protocol
- Play2
- 파이썬 동시성
- 하이퍼레저 패브릭
- 스칼라
- 안드로이드 웹뷰
- Today
- Total
HAMA 블로그
Rust (async/Future) vs Kotlin Coroutines vs Java Virtual Threads 차이 [기초] 본문
Rust (async/Future) vs Kotlin Coroutines vs Java Virtual Threads 차이 [기초]
[하마] 이승현 (wowlsh93@gmail.com) 2024. 12. 20. 14:49Rust vs Kotlin vs Java
Rust의 컴파일 타임 vs Kotlin/Java의 런타임
특징 | Rust (async / Future) | Kotlin Coroutines | Java Virtual Threads |
비동기 실행 모델 | Future 기반 (poll-driven) | Suspend 함수 기반 (Continuation) | 사용자 모드 스레드 기반 (Thread API) |
상태 관리 | 컴파일 타임에 상태 기계로 변환 | 런타임에서 상태를 관리 | 런타임에서 사용자 모드 스레드 관리 |
최적화 | 컴파일 타임 최적화 (zero-cost abstraction) | 일부 컴파일 타임, 주로 런타임 최적화 | 주로 런타임 최적화 |
런타임 필요성 | 런타임 스케줄러 필요 (Tokio, async-std 등) | 코루틴 디스패처 필요 | Java 런타임 자체에서 관리 |
제어권 | 개발자에 의해 언어&라이브러리 차원의 키워드로 명시적 으로 제어권 반환해야함 | 개발자에 의해 언어&라이브러리 차원의 키워드로 명시적으로 제어권 반환 해야함 | 명시적 제어권 반환하는 키워드가 새로 생긴건 없으며, 기존 자바 동기 코드를 사용해도 내부에서 알아서 비동기 처리 |
난이도 | 중간 | 중간 | 쉬움 & 세밀한 제어 및 부모-자식관계 관리등 안됨 |
Rust의 컴파일 타임
Rust의 비동기 프로그래밍 모델에서는 컴파일 타임에 많은 작업을 수행하여 효율적인 비동기 실행을 지원합니다. Rust의 비동기 실행 모델은 async/await 키워드와 Future 트레이트를 기반으로 하며, 다른 언어들에 비해 컴파일 타임 최적화가 더 강력한 특징을 가지고 있습니다.
Rust의 async와 Future 트레이트
- Rust에서 **async fn**은 **컴파일 타임에 상태 기계(state machine)**로 변환됩니다.
- async fn이 호출되면 미리 생성된 상태 기계 객체가 반환되며, 이 객체는 Future 트레이트를 구현합니다.
- Future는 poll 기반의 실행 모델로, 런타임에서 필요한 시점에 실행됩니다.
Rust의 강점: 컴파일 타임에 많은 것을 결정
- Rust는 async fn을 Future 객체로 변환하여, 비동기 함수의 모든 상태를 컴파일 타임에 정의합니다.
- 이는 런타임 오버헤드를 줄이고, 높은 성능과 안전성을 보장합니다.
- 비동기 코드의 런타임 비용을 최소화하기 위해 설계되었으며, 이 점에서 Kotlin이나 Java보다 더 낮은 레벨의 최적화를 제공합니다.
사실 위의 말 가지고는 이해 할 수 가 없습니다. 세상은 좋은 선생님들 덕분에 돌아갑니다. :-)
아래에 직접 구현된 예시가 있습니다. (컴파일 타임에 코루틴이 구현되는 방식 - 예제 코드 솔직히 킹왕짱.)
https://github.com/cfsamson/Asynchronous-Programming-in-Rust/tree/main/ch08
핵심코드만 발췌)
// coroutine fn async_main() {
// println!("Program starting");
// let txt = http::Http::get("/600/HelloAsyncAwait").wait;
// println!("{txt}");
// let txt = http::Http::get("/400/HelloAsyncAwait").wait;
// println!("{txt}");
// }
// 위의 코드가 아래 코드로 컴파일 시점에 변경 //
// 설명) 상태를 가진 코루틴 객체가 첫번째 wait만나기 전까지 상태를 저장하고, 자식 비동기 호출하고
// 이런식으로 함수가 특정 위치부터 이전 상태를 기반으로 재실행됨
fn async_main() -> impl Future<Output=String> {
Coroutine0::new()
}
enum State0 {
Start,
Wait1(Box<dyn Future<Output = String>>),
Wait2(Box<dyn Future<Output = String>>),
Resolved,
}
struct Coroutine0 {
state: State0,
}
impl Coroutine0 {
fn new() -> Self {
Self { state: State0::Start }
}
}
impl Future for Coroutine0 {
type Output = String;
fn poll(&mut self, waker: &Waker) -> PollState<Self::Output> {
loop {
match self.state {
State0::Start => {
// ---- Code you actually wrote ----
println!("Program starting");
// ---------------------------------
let fut1 = Box::new( http::Http::get("/600/HelloAsyncAwait"));
self.state = State0::Wait1(fut1);
}
State0::Wait1(ref mut f1) => {
match f1.poll(waker) {
PollState::Ready(txt) => {
// ---- Code you actually wrote ----
println!("{txt}");
// ---------------------------------
let fut2 = Box::new( http::Http::get("/400/HelloAsyncAwait"));
self.state = State0::Wait2(fut2);
}
PollState::NotReady => break PollState::NotReady,
}
}
State0::Wait2(ref mut f2) => {
match f2.poll(waker) {
PollState::Ready(txt) => {
// ---- Code you actually wrote ----
println!("{txt}");
// ---------------------------------
self.state = State0::Resolved;
break PollState::Ready(String::new());
}
PollState::NotReady => break PollState::NotReady,
}
}
State0::Resolved => panic!("Polled a resolved future")
}
}
}
}
Kotlin/Java의 런타임
코틀린 코루틴의 협력적 스케줄링 (Cooperative Scheduling)
특징:
- 스레드나 작업이 자발적으로 제어권을 넘겨주는 방식으로 동작합니다.
- 작업 중간에 명시적으로 제어권을 반환하는 지점 (yield, await, suspend 등)을 코드에 삽입해야 합니다.
- 런타임 스케줄러는 이 반환 지점에서 다른 작업으로 전환합니다.
장점:
- 컨텍스트 스위칭 비용이 매우 낮음: 협력적 스케줄링은 커널의 간섭 없이 사용자 공간에서 이루어지므로 매우 빠름.
- 개발자가 작업 흐름을 더 세밀하게 제어할 수 있음.
- 대량의 경량 작업을 실행할 때 효율적.
단점:
- 비협력적인 코드가 있다면 문제가 발생: 작업이 제어권을 반환하지 않으면, 다른 작업이 실행되지 못하고 시스템이 멈출 수 있음.
- 협력적 스케줄링은 블로킹 작업을 자체적으로 처리하지 못하므로, I/O 작업 등을 비동기로 처리해야 함.
동작 방식
- 협력적 스케줄러가 관리하는 작업은 사용자 공간에서 실행됩니다.
- 각 작업은 명시적으로 제어권을 반환해야 다른 작업이 실행될 수 있습니다.
- 모든 작업은 동일한 스레드에서 실행될 수 있습니다 (멀티스레드 환경도 가능).
import kotlinx.coroutines.*
import kotlin.random.Random
suspend fun fetchData(id: Int): String {
delay(Random.nextLong(100, 500)) // 비동기 대기 (명시적 제어권 반환)
return "Data for request $id"
}
fun main() = runBlocking {
val jobs = List(100) { id -> // 100개의 코루틴 생성
launch {
val data = fetchData(id)
println("Coroutine $id: $data on thread ${Thread.currentThread().name}")
}
}
jobs.forEach { it.join() } // 모든 작업이 끝날 때까지 대기
}
delay() 호출 시 제어권을 반환해 다른 작업이 실행됩니다.
Java Virtual Thread - OS 커널 스레드와 분리된 사용자 모드 스레드 (User-mode Threads)
특징:
- 사용자 모드 스레드는 OS 커널 스레드와 독립적으로 관리됩니다.
- Java Virtual Threads와 같이 사용자 공간에서 구현된 가벼운 스레드입니다.
- 작업 실행은 OS의 스케줄러가 아닌 사용자 모드 스케줄러에서 관리됩니다.
장점:
- OS 커널 스레드의 제한 없이 대량의 사용자 모드 스레드를 실행할 수 있음.
- 스레드 생성과 스위칭이 커널 스레드보다 훨씬 빠름.
- 기존의 동기 프로그래밍 모델(블로킹 호출 포함)을 유지하면서 비효율을 제거 가능.
단점:
- I/O 작업이나 블로킹 호출은 내부적으로 비동기로 처리되어야 함.
- 커널 스레드에 비해 구현이 복잡할 수 있음.
동작 방식
- 사용자 모드 스케줄러는 사용자 모드 스레드를 관리하며, 이 스레드는 필요 시 커널 스레드에서 실행됩니다.
- 사용자 모드 스레드가 실행을 중단하면(예: I/O 대기) 커널 스레드가 다른 사용자 모드 스레드를 실행합니다.
- 이 과정은 투명하게 이루어지며, 기존 동기 코드를 그대로 사용할 수 있습니다.
Java의 Virtual Threads는 사용자 모드 스레드입니다.
- **Thread.startVirtualThread**를 통해 가벼운 사용자 모드 스레드를 생성합니다.
- 내부적으로 스케줄러가 I/O 대기 및 실행을 관리하며, OS 스레드가 비효율적으로 차단되지 않습니다.
import java.util.concurrent.*;
import java.util.Random;
public class Main {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Random random = new Random();
for (int i = 0; i < 100; i++) { // 100개의 Virtual Threads 생성
int id = i;
executor.submit(() -> {
try {
Thread.sleep(random.nextInt(400) + 100); // 블로킹 호출
System.out.println("Virtual Thread " + id + ": Data fetched on thread " + Thread.currentThread().getName());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES); // 모든 작업이 끝날 때까지 대기
}
}
협력적 스케줄링과 사용자 모드 스레드의 차이
특징협력적 스케줄링사용자 모드 스레드
작업 전환 방식 | 작업이 명시적으로 제어권을 반환해야 함 | 사용자 모드 스케줄러가 작업 전환을 관리 |
스케줄링 | 비선점형 (Non-preemptive) | 선점형 (Preemptive) |
I/O 처리 | 비동기 I/O와 결합하여 동작 | 내부적으로 비동기 I/O 처리, 블로킹 호출과 호환 |
스레드 관리 | 단일 OS 스레드에서 실행 가능 | 여러 사용자 모드 스레드가 OS 스레드에서 매핑되어 실행 |
사용 예시 | Kotlin 코루틴, Python async/await | Java Virtual Threads, Go 언어의 goroutine |
성능 | 컨텍스트 스위칭 비용이 낮음 | 더 많은 스레드를 처리할 수 있지만 스위칭 비용이 더 높음 |
코드 스타일 | 비동기 코드 (async/await, suspend) | 동기 스타일과 비동기 스타일 모두 지원 |
Rust Future와 다른 모델 비교
Rust Future | Kotlin Coroutine | Java Virtual Threads | Go Goroutine | |
스케줄링 방식 | 협력적 스케줄링 | 협력적 스케줄링 | 선점형 스케줄링 | 선점형 스케줄링 |
제어권 반환 | 명시적 (Poll::Pending) | 명시적 (suspend, yield) | 자동 | 자동 |
실행 트리거 | Lazy (스케줄러 필요) | Lazy (런타임 필요) | Eager (즉시 실행 가능) | Eager (즉시 실행 가능) |
I/O 처리 방식 | 논블로킹 | 논블로킹 | 동기 호출도 비동기로 처리 가능 | 논블로킹 |
스케줄러 제어 가능성 | 개발자가 직접 제어 (poll 호출) | Dispatchers로 스케줄링 제어 | JVM 스케줄러 자동 관리 | Go의 GMP 스케줄러 자동 관리 |
사용 스타일 | 비동기 (async/await, poll) | 비동기 (async/await) | 동기 (Thread API와 동일) | 동기 (go 키워드) |
별책부록1)
Java Virtual Threads와 Kotlin Coroutines 모두 결국 JVM 스레드 풀(예: ForkJoinPool) 위에서 실행됩니다.
둘 모두 OS Thread를 직접 사용 할 때 이루어지는 블로킹 및 컨텍스트 스위칭을 줄여서 동시성 처리 효율을 극대화 하는 방향으로 설계됩니다. Java Virtual Threads 를 사용하면 학습곡선이 낮아집니다. 명시적인 동시성 처리를 안하니깐~
대신 디테일한 조정을 못하게 되겠지요. Go언어의 Goroutine생각하면 Kotlin Coroutine보다 Java Virtual Threads가 훨씬 잘 쓰일듯. (한데.. 코프링이 워낙 좋아서 또.. 몰러)
좀 편하게 살자 !!! (https://tech.kakaopay.com/post/ro-spring-virtual-thread/)
JVM과 OS 스레드 관계
- JVM은 기본적으로 OS 스레드를 기반으로 동작합니다.
- Java에서의 **스레드(Thread)**는 OS 커널 스레드에 직접 매핑됩니다.
- 따라서 Java의 스레드 API를 통해 생성한 스레드는 JVM에서 OS 스레드로 매핑되어 커널 스케줄러에 의해 관리됩니다.
별책부록2)
Kotlin Coroutines에서는 간단히 처리할 수 있지만 Java Virtual Threads로는 더 많은 코드와 복잡한 제어가 필요한 경우의 예제
시나리오 1: 부모-자식 관계로 작업 그룹 관리
- 문제:
- 100개의 작업을 병렬로 실행하지만, 특정 조건에서 중간에 멈춰야 합니다(예: 하나의 작업이 실패하면 전체 작업을 취소).
- 모든 작업이 끝난 후, 결과를 종합해서 처리해야 합니다.
- 목표:
- 작업의 그룹 단위 관리 (부모-자식 관계).
- 작업 중 하나라도 실패하면 나머지 작업을 취소.
- 모든 작업이 성공하면 결과를 합산.
시나리오 2: 병렬 작업에서 타임아웃과 취소 관리
- 문제:
- 100개의 작업을 병렬로 실행하며, 각각 최대 300ms 안에 완료되어야 합니다.
- 작업이 실패하거나 타임아웃이 발생하면, 해당 작업을 취소하고 나머지 작업은 계속 진행합니다.
- 결과를 모두 모아 성공적으로 완료된 작업만 처리합니다.
Kotlin Coroutines 구현
Kotlin 코루틴은 **구조화된 동시성(Structured Concurrency)**를 제공하므로, 부모-자식 관계를 자연스럽게 관리할 수 있습니다. coroutineScope나 supervisorScope를 사용해 간단히 구현 가능합니다.
//// 시나리오 1
import kotlinx.coroutines.*
import kotlin.random.Random
suspend fun fetchData(id: Int): Int {
delay(Random.nextLong(100, 500)) // 비동기 대기
if (Random.nextBoolean()) throw RuntimeException("Error in task $id") // 실패 가능성
return id * 10
}
fun main() = runBlocking {
try {
val results = coroutineScope { // 부모 코루틴 스코프
(1..100).map { id ->
async { fetchData(id) } // 자식 코루틴 실행
}.awaitAll() // 모든 결과를 기다림
}
println("All tasks completed successfully: ${results.sum()}")
} catch (e: Exception) {
println("Failed: ${e.message}")
}
}
//// 시나리오 2
import kotlinx.coroutines.*
import kotlin.random.Random
suspend fun fetchData(id: Int): Int {
delay(Random.nextLong(100, 500)) // 비동기 대기
if (Random.nextBoolean()) throw RuntimeException("Error in task $id") // 실패 가능성
return id * 10
}
fun main() = runBlocking {
val results = (1..100).map { id ->
async {
try {
withTimeout(300) { fetchData(id) } // 300ms 안에 완료되지 않으면 취소
} catch (e: Exception) {
null // 실패하거나 타임아웃 발생 시 null 반환
}
}
}.awaitAll()
val successfulResults = results.filterNotNull()
println("Successful results: ${successfulResults.sum()}")
}
Kotlin 코루틴 동작 방식:
- **coroutineScope**는 부모 코루틴으로, 자식 코루틴들의 생명 주기를 관리합니다.
- 작업 중 하나라도 실패하면, 나머지 작업을 자동으로 취소합니다.
- 결과를 awaitAll()로 간단히 모아서 처리합니다.
Java Virtual Threads 구현
Java Virtual Threads는 구조화된 동시성을 기본적으로 제공하지 않으므로, 비슷한 기능을 구현하려면 더 많은 코드와 수작업이 필요합니다
/// 시나리오 1
import java.util.concurrent.*;
import java.util.*;
import java.util.stream.*;
import java.util.Random;
public class Main {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
List<Future<Integer>> futures = new ArrayList<>();
Random random = new Random();
// 부모 작업: 결과 수집
try {
for (int i = 1; i <= 100; i++) {
final int id = i;
futures.add(executor.submit(() -> {
try {
Thread.sleep(random.nextInt(400) + 100); // 블로킹 대기
if (random.nextBoolean()) throw new RuntimeException("Error in task " + id); // 실패 가능성
return id * 10;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}));
}
// 결과 처리
int sum = 0;
for (Future<Integer> future : futures) {
sum += future.get(); // 작업 완료 대기 및 결과 수집
}
System.out.println("All tasks completed successfully: " + sum);
} catch (ExecutionException e) {
System.out.println("Failed: " + e.getCause().getMessage());
// 자식 작업 취소
for (Future<Integer> future : futures) {
future.cancel(true);
}
} finally {
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
}
}
}
/// 시나리오 2
import java.util.concurrent.*;
import java.util.*;
import java.util.stream.*;
import java.util.Random;
public class Main {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Random random = new Random();
List<Future<Integer>> futures = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
final int id = i;
futures.add(executor.submit(() -> {
try {
Thread.sleep(random.nextInt(400) + 100); // 블로킹 대기
if (random.nextBoolean()) throw new RuntimeException("Error in task " + id);
return id * 10;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}));
}
List<Integer> results = new ArrayList<>();
for (Future<Integer> future : futures) {
try {
results.add(future.get(300, TimeUnit.MILLISECONDS)); // 300ms 타임아웃 설정
} catch (TimeoutException | ExecutionException e) {
future.cancel(true); // 타임아웃 시 작업 취소
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Successful results: " + results.stream().mapToInt(Integer::intValue).sum());
executor.shutdown();
}
}
Java Virtual Threads 동작 방식:
- Virtual Threads는 ExecutorService에서 관리되며, 각 작업은 Future 객체로 반환됩니다.
- 작업 실패 시 예외를 처리하려면 추가적인 로직(try-catch와 future.cancel())이 필요합니다.
- 구조화된 동시성이 제공되지 않으므로, 부모-자식 관계를 직접 관리해야 합니다.
'소프트웨어 사색 ' 카테고리의 다른 글
고품질 코드 (1) - 기존 코드 건드리지 말기 (feat. 코틀린) (0) | 2023.06.05 |
---|---|
새로운 언어를 공부해야 하는 이유 (feat.코틀린) (0) | 2023.06.05 |
내 언어의 한계가 내 세계의 한계다 (1) | 2022.08.30 |
잘 짠 코드란?? (0) | 2022.05.07 |
SI,정부과제 vs 서비스 vs 솔루션 장단점 (1) | 2022.03.14 |