코루틴의 경량성을 실험하게 된 계기
코틀린을 써보기도 전에 많이 듣던 얘기는 코틀린의 코루틴이 좋다는 이야기였습니다. 그런 이야기를 늘으니 문득 궁금증이 생겼습니다. 얼마나 좋길래 이렇게 유명하지?!
아무래도 궁금하다면 직접 테스트해보는 게 가장 좋지 않을까란 생각에 직접 멀티스레드와 비교해보면서 테스트 해보려 합니다.
코루틴이 코틀린 말고도 다른 언어에서도 지원하는 기술자체의 명칭이란 건 나중에 알게 되었습니다;; 물론 코틀린은 라이브러리나 프레임워크가 아닌 언어 차원에서 지원한다는 차이점이 존재하긴 합니다.
멀티스레드 VS 코루틴
비교 조건은 아래와 같습니다.
- 코루틴과 멀티 스레드 모두 사용 가능한 모든 스레드를 사용한다.
- 멀티스레드와 코루틴 모두 1만 개의 작업을 생성한다.
- 하나의 작업당 0.01초의 Sleep을 준다.
- 시작 시 메모리와 작업 종료 직후 메모리를 비교해서 메모리를 측정한다.
멀티스레드 코드
fun threadTask() {
Thread.sleep(10)
}
fun measureThreadPerformance() {
val threadCount = 10000
val executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
val startMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
for (i in 0 until threadCount) {
executor.submit {
threadTask()
}
}
executor.shutdown()
executor.awaitTermination(1, TimeUnit.MINUTES)
val endMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
println("Threads - Memory used: ${(endMemory - startMemory) / 1024} KB")
}
fun main() {
measureThreadPerformance()
}
결과 (5회 평균):
Threads - Memory used: 6588 KB
Threads - Memory used: 6138 KB
Threads - Memory used: 6588 KB
Threads - Memory used: 6145 KB
Threads - Memory used: 5238 KB
평균: 6139.4 KB
코루틴 코드
suspend fun coroutineTask() {
delay(10)
}
fun measureCoroutinePerformance() = runBlocking {
val coroutineCount = 10000
val startMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
val jobs = List(coroutineCount) {
launch {
coroutineTask()
}
}
jobs.forEach { it.join() }
val endMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
println("Coroutines - Memory used: ${(endMemory - startMemory) / 1024} KB")
}
fun main() {
measureCoroutinePerformance()
}
결과 (5회 평균):
Coroutines - Memory used: 4772 KB
Coroutines - Memory used: 5224 KB
Coroutines - Memory used: 4772 KB
Coroutines - Memory used: 4772 KB
Coroutines - Memory used: 4772 KB
평균: 4862.4 KB
코루틴이 약 20% 정도 더 적은 메모리를 사용하는 것을 확인할 수 있었습니다.
의문점
- 코루틴은 아무리 반복해도 5224KB 혹은 4772KB 두 가지 값만 확인됨.
- 실행 속도도 코루틴이 체감상 훨씬 빠름.
얼마나 더 빠른가?
시간 측정을 추가한 결과:
멀티스레드
Threads - Time taken: 13282 ms
Threads - Time taken: 14081 ms
Threads - Time taken: 13770 ms
Threads - Time taken: 13503 ms
Threads - Time taken: 13347 ms
평균: 13596.6 ms
코루틴
Coroutines - Time taken: 153 ms
Coroutines - Time taken: 153 ms
Coroutines - Time taken: 154 ms
Coroutines - Time taken: 141 ms
Coroutines - Time taken: 131 ms
평균: 146.4 ms
약 93배 가까운 속도 차이 발생
이게 말이 되나?
코루틴은 운영체제 스레드가 아닌 사용자 수준 스레드로 관리되며, 스레드 스위칭 오버헤드가 적음. 하지만 아무리 그래도 차이가 너무 크다는 느낌.
그 이유는 바로 Thread.sleep(10)
과 delay(10)
의 동작 방식 차이 때문!
Thread.sleep(10)
은 스레드를 차단(blocking)delay(10)
은 스레드를 차단하지 않고 비동기로 기다림 (suspending)
결과적으로 코루틴은 스레드를 차단하지 않기 때문에 훨씬 더 빠르게 동작함.
CPU를 많이 사용하는 작업 시엔 어떻게 될까?
변경된 Task 코드:
fun task() {
var result = 0L
for (i in 1..1_000_000) {
result += i
}
}
결과
멀티스레드
Threads - Time taken: 427 ms
Threads - Time taken: 474 ms
Threads - Time taken: 421 ms
Threads - Time taken: 420 ms
Threads - Time taken: 602 ms
평균: 468.8 ms
코루틴
Coroutines - Time taken: 3452 ms
Coroutines - Time taken: 3579 ms
Coroutines - Time taken: 3657 ms
Coroutines - Time taken: 3567 ms
Coroutines - Time taken: 3620 ms
평균: 3575.0 ms
CPU 연산이 많은 작업에서는 멀티스레드가 약 8배 더 빠름
코루틴은 왜 메모리 사용량이 일정한가?
- 멀티스레드는 각각의 스레드가 자체 스택 메모리를 보유하고 있으며, JVM의 GC와 메모리 변동성에 따라 사용량이 들쭉날쭉함.
- 코루틴은 힙에 메모리를 할당하여 보다 일정하고 효율적인 메모리 사용이 가능.
정확한 원인이라기보단, 여러 글을 읽고 내린 개인적인 결론입니다. 더 정확한 정보를 아시는 분은 댓글로 알려주시면 감사하겠습니다!
결론
- ✅ 코루틴은 멀티스레드에 비해 더 작은 메모리를 사용함
- ✅ 일반적인 작업에서는 코루틴이 훨씬 빠르지만, CPU를 많이 사용하는 작업에서는 오히려 멀티스레드가 유리할 수도 있음
이상입니다. 긴 글 읽어주신 분들께 감사합니다! 🙏