들어가며
안녕하세요! java21 LTS version 이 신규 출시됨에 따라 많이 관심을 받고 있는 Virtual thread 에 대해 이야기해보고자 하는데요
Virtual thread 는 기존에 동작하는 Platform thread 에서 동작하며, I/O blocking 발생시 중단(suspend) 지점을 stack 으로 옮겨, I/O block 되는 시간동안 다른 일들을 더 수행할 수 있게 지원하는 경량화 쓰레드라고 할 수 있습니다.
따라서 java 진영에서는 기존에 thread-per-request style 를 갖춘 application server 에서 I/O blocking 으로 인해 아무것도 하는일이 없는데도 task 를 처리할 수 없는 상황들로 인해 병목이 되는 부분들을 해결할 수 있어 굉장히 각광받게 되었는데요.
어떤점 때문에 각광받게 되었는지 알아봅시다.
Virtual thread
Goal
우선 Virtual thread 의 목표에 대해 우리는 이해해야 됩니다.
목표를 이해하는 것은 매우 중요합니다. 목표라는 것은 기술의 개발 방향성을 결정해주는 중요한 척도가 되기 때문인데요
아래는 Virtual thread 를 만들었던 Project Loom 의 목표를 가져왔습니다.
1. Enable server applications written in the simple thread-per-request style to scale with near-optimal hardware utilization
2. Enable exsiting(legacy) code that uses the java.lang.Thread API to adopt virtual threads with minimal change
3. Enable easy troubleshooting, debugging, and profiling of virtual threads with existing JDK tool
목표에서 언급되었듯이 이미 운영중인 thread-per-request style 의 application server 에 기존에 사용하던 방식에서 큰 변화없이 Virtual thread 를 도입하기 쉽게 하겠다 하는 것이 목표입니다.
새로운 API 를 통해 제공하는 것이 아닌 하위호환성을 쉽게 가져가는 선에서 최대한의 기능을 제공하겠다는 것이죠
Example
예시와 함께 이해해볼게요
아래와 같이 thread-pool 을 만들어 처리하는 로직이 있다고 가정해보겠습니다.
fun main() {
val executors = Executors.newFixedThreadPool(10)
val elapsed = measureTimeMillis {
val futures = (1..100).map {
executors.submit {
Thread.sleep(500)
}
}
futures.forEach { it.get() }
}
println("took $elapsed ms")
}
thread-pool 이 10개라는 가정하에 100개의 task 를 한다고 가정하고, 1개의 task 당 500ms 가 걸린다고 쳐봅시다.
그러면 전체를 수행하는데 시간이 얼마나 걸릴까요?
기본 계산만 하더라도 5초가 걸리게 되죠
목표에서 설명한대로 virtual thread 로 쉽게 바꾸어보면 어떻게 될까요?
fun main() {
val executors = Executors.newVirtualThreadPerTaskExecutor()
val elapsed = measureTimeMillis {
val futures = (1..100).map {
executors.submit {
Thread.sleep(500)
}
}
futures.forEach { it.get() }
}
println("took $elapsed ms")
}
Executors.newVirtualThreadPerTaskExecutor 를 이용하여 virtual thread 를 사용하도록 수정해준 아주 간단한 개선사항인데요
결과는 놀랍게도...
한 Task 를 처리하는 시간만큼 걸리게되는 것이죠
왜일까요?
Principle
Virtual thread 는 Platform thread 위에서 동작합니다.
단, Blocked 상태가 된다면 heap 에 mount 하여 임시로 저장하고 다른 task 들을 수행하도록 지시하게 되는 것이죠
따라서 위 예시에서 몇 개의 task 를 처리한다 하더라도 다 heap 에 데이터를 쌓아두어 block 되는 thread 들을 저장하기 때문에 전체 처리시간은 동일하게 됩니다
IO 에 대한 대기 처리를 Heap 을 통해 저장하고 있다가 처리하기 때문에,
주어진 Platform thread(ForkJoinPool의 thread)를 별도의 대기시간 없이 처리를 진행하게 되는 것이죠
Limit
단, 이렇게 좋아보이는 Virtual thread 의 heap 으로 mount 하는 기능도 한계점이 있습니다.
- Running inside native method
- Running inside synchronized sections
native method 와 synchronized section 속에서는 mount 를 할 수 없습니다.
왜냐하면 native method, synchronized 는 둘다 platform thread 를 pinning 할 수 밖에 없는 상황에 처하기 때문입니다.
즉, thread 를 고정을 할 수 밖에 없는 상황으로 구현이 되어 있다는 것입니다.
두 기능 모두 JVM 수준의 level 에서 구현이 되어 있기 때문에 java 언어 레벨로 구현이 가능한 상황이 아니기 때문입니다.
예를 들면, synchronized 는 monitor 기반의 object 로 thread 를 blocking 하는 연산으로 구현이 되어 있기 때문에 언어 레벨에서 건드릴 수 있는 영역이 아닌 것이죠
이러한 한계점들은 존재하지만 I/O blocking 이 많은 곳,
특히나 요즘 같이 MSA(microservice architecture) 기반으로 외부서버와의 통신이 많아지고 있는 현재 추세를 고려하여
설계된 것이 아닐까 생각은 드네요
java 진영에서 이렇게나 발전하고 있는데, 같은 jvm 을 사용하는 kotlin 에서는 이미 coroutine 이라는 API 를 통해 많은 관심을 받았었기에 kotlin 과 java 를 비교하면서 자연스럽게 coroutine 까지 비교해보는 사람들이 많아졌습니다.
하지만 우리는 2개의 목적이 다르다는 것을 먼저 인지할 필요가 있습니다.
Kotlin Coroutine
Goal
kotlin coroutine 의 목표는 무엇일까요?
1. Make it possible to utilize Kotlin coroutines as wrappers for different exsiting asynchronous APIs (such as Java NIO, different implementations of Futures, etc)
2. No dependency on particular implementation of Futures or such rich library
3. Cover equally the async/await use case and generators block
놀랍게도 Coroutine 의 목표는 비동기 API 를 kotlin 으로 wrapping 하여 비동기에 대한 추상화를 한다는 것입니다.
따라서 Virtual thread 와 목표 자체가 다르다는 것을 확인할 수 있습니다.
Principle
Coroutine 이 제공하는 여러 기능들을 이해하기 위한 그림을 가져왔는데요.
보시다싶이 모두 비동기에 대한 동작 방식을 언어 레벨로 추상화하여 제공했다는 것을 이해하기 위한 그림으로 봐주시면 되요
예를 들면, launch, async-await 과 같은 기능들이 thread 를 넘나들면서 task 를 처리하는 것을 볼 수 있죠.
서로 비교할만한 대상은 아니지만 조금 더 비교를 해봅시다
Virtual Thread | Coroutine | |
Implementation | Inside the VM | Inside the Kotlin compiler |
Cost | yield cost: high call cost: low |
yield cost: low suspend call cost: high |
Memory | High | Low |
Virtual Thread 는 yield(waiting) 이 발생하면 heap 메모리에 바로 올리기 때문에, 메모리를 사용하는 비용이 비쌀 수 밖에 없습니다.
반대로 Coroutine 은 yield(waiting) 에 대한 중단점을 state 로 기록하기 때문에 메모리 사용은 없고 cpu 연산을 더 쓰게 되는 것입니다
Example
그러면 Virtual Thread 를 Coroutine 에 접목시킬 방법이 있을까요?
위에서 설명했듯이 Virtual Thread 는 I/O blocking 을 해결하는데 아주 큰 효과를 볼 수 있는데요
suspend fun main() {
val dispatcher = Dispatchers.IO
coroutineScope {
val elapsed = measureTimeMillis {
val jobs = (1..100_000).map {
async(dispatcher) {
delay(500)
}
}
awaitAll(*jobs.toTypedArray())
}
println("took $elapsed ms")
}
}
참고로, Dispatchers.IO 는 Platform thread 를 64개 만들고 운용하는 thread pool 로 이해해주시면 됩니다.
10만개의 task 를 500ms 걸린다고 가정했을 때, 결과는.!
이번에는 이를 Virtual thread 로 운영하는 dispatcher 를 만들어보겠습니다.
suspend fun main() {
val dispatcher = Executors.newVirtualThreadPerTaskExecutor().asCoroutineDispatcher()
coroutineScope {
val elapsed = measureTimeMillis {
val jobs = (1..100_000).map {
async(dispatcher) {
delay(500)
}
}
awaitAll(*jobs.toTypedArray())
}
println("took $elapsed ms")
}
}
이를 가동해보면 결과는..!
Platform thread 보다 더 가볍기 때문에 더 좋은 performance 를 나타내게 됩니다.
참고로 coroutine 자체가 비동기로 흘러가다보니 실행되는 컴퓨터 사양에 따라 측정치는 다르게 나올 수 있습니다.
또 하나 coroutine 은 근본적으로 structured concurrency 컨셉을 제대로 활용하였다고 언급되는데요
이는 coroutineScope 가 기본적으로 2가지 성격을 띄고 있기 때문입니다.
- Parent - Child hierarchy
- Cancellation and error-handling
이는 제가 coroutine 에서의 예외 처리에 대해 설명한 글이 있는데 참고해보시면 좋겠네요
물론 java 진영에서도 Structured concurrency 가 java 21 의 preview 로 나왔다는 소식이 들려오네요
서로가 어떻게 다를지 기대되는군요
정리하며
오늘은 Virtual Thread 에 대해 알아보고 Coroutine 에 대해서도 알아보았는데요
기술이 점점 발전함에 따라 I/O 가 많아지는 상황속에서 어떻게 하면 더 성능을 최대한 이끌어내볼 수 있을지 에 대한 결과물들을 비교한 것 같네요
앞으로 발전되는 기술이 점점 기대되고, Virtual thread 가 많은 부분에서 상용화되어 Kotlin coroutine 처럼 널리 쓰일 수 있는 날이 오면 좋겠네요~!!
참고
Coroutines and Loom behind the scenes by Roman Elizarov
Open jdk - JEP 444: Virtual Threads
Kotlin - Coroutines basic
'Developer > Kotlin & Java' 카테고리의 다른 글
SocketException: Connection reset 과 ClientAbortException 은 왜 발생할까? (0) | 2024.12.01 |
---|---|
[Kotlin] Coroutine - 6. Suspend 함수에 대해 deep dive 를 해보자 (3) | 2024.03.01 |
[Kotlin] Kotlin Annotation 에 대해 톧아보기 (0) | 2023.04.01 |
[Kotlin] Coroutine - 5. 코루틴의 Channel 의 모든 것 (0) | 2023.03.05 |
[Kotlin] Coroutine - 4. 코루틴에서의 예외(exception) 핸들링 (0) | 2023.02.04 |