-
Kotlin Coroutines🔖 Kotlin 2024. 12. 28. 00:44
🤔 Kotlin Coroutines
Coroutine 이란?
- co(함께) + routine (특정 일을 수행하는 명령의 모음)
- 코틀린에서 제공하는 기능으로 비동기 작업을 효율적으로 처리하기 위해 설계된 경량화된 동시성 처리 방식
📍 특징
- suspend(중단)와 resume(재개)가 가능
- 비 선점형 멀티태스킹 (Non-preemptive multitasking)
- 협력형 멀티태스킹 (Cooperative multitasking)
❗ 왜 코루틴을 사용해야 할까요?
📖 비동기 처리
- 네트워크 요청, DB 작업, 파일 I/O 처럼 오랜 시간이 걸리는 작업을 비동기로 처리하는 것이 중요
코루틴은 Kotlin 에서 동시성을 간단하게 처리할 수 있는 기능 중 하나.
- 비동기 코드를 동기적으로 작성하는 것처럼 보이게해 코드의 가독성을 높이고 복잡한 콜백이나 다른 동시성 패턴에 비해 쉽게 작성할 수 있게 한다.
- 코루틴은 중단할 수 있으며, 중단될 때 스레드를 블로킹하지 않고 다른 작업에 양보한다. 따라서 스레드를 다른 작업을 수행시키는데 사용할 수 있다.
🆚 Subroutine
Subroutine Coroutine Routine : 특정한 일을 처리하기 위한 명령 일시 중단과 재개가 가능한 작업 단위
- 특정 지점에서 작업을 멈추고, 이후 필요할 때 재개Subroutine : Routine의 하위에서 실행되는 또 다른 루틴 중단된 곳에서 다시 시작가능
- 여러개의 진입점을 가진다서브 루틴은 하나의 진입점을 가지며, 한 번 호출되면 작업을 완료할 때까지 멈추지 않고 쭉 실행되어 스레드가 다른 작업을 할 수 없다 작업을 하지 않을 때는 스레드 사용 권한을 양보하며 서로 협력적으로 실행 🏃 동작 방식
Q. 코루틴은 어떻게 중단과 재개를 할까?
⇒ CPS (continuation Passing Style)
CPS란 호출되는 함수에 Continuation을 전달하고 각 함수의 작업이 완료되는 대로 전달받은 Continuation을 호출하는 패러다임이다.
@SinceKotlin("1.3") public interface Continuation<in T> { public val context: CoroutineContext public fun resumeWith(result: Result<T>) }
- Continuation : 각 중단 시점마다 코루틴의 현재 상태(실행 지점, 로컬 변수 등)와 다음에 무슨 일을 해야 할지를 기억하는 확장된 콜백 역할을 하는 객체
Continuation 인터페이스에는 크게 context 객체와 resumeWith 함수가 있다.
- resumeWith : 특정 함수 a가 suspend 되어야 할 때, 현재 함수에서 a의 결과를 T로 받게 해주는 함수
- 실행을 재개하기 위한 메서드
- context: 각 Continuation이 특정 스레드 혹은 스레드 풀에서 실행되는 것을 허용해준다.
- dispatcher, job 등 ConroutineContext를 저장하는 변수
📍 정리
CPS에서 Continuation을 전달하면서 현재 suspend된 부분에서의 resume을 가능하게 해준다.
- Continuation은 resume 되었을 때의 동작 관리를 위한 객체
Q. Continuation 객체는 어떻게 생기나?
⇒ 컴파일러가 suspend function을 만나게 되면 내부적으로 Continuation을 활용하도록 변환
🏃 컴파일러의 suspend function 변환 과정
// 예제 suspend fun test() { println("Hello") delay(1000L) // 코루틴에서 제공하는 일시 중단 함수 println("World!") } suspend fun delay(timeMillis: Long)
1. 컴파일러가 함수 내부의 중단 가능 지점 을 식별
- 위 코드에서의 중단 가능 지점
- suspend fun test()
- delay(1000L)
- suspend fun delay()
2. 유한 상태 머신(Finite State Machine, FSM) 으로 변환
suspend fun test() { when (label) { 0 -> { println("Hello") delay(1000L) } 1 -> { println("World!") } } }
- 코틀린은 모든 중단 가능 지점을 찾아 when으로 표현
- 중단 가능 지점을 기준으로 코드 블록을 분리하고 분리된 코드들은 label로 구별
- label은 0부터 증가하고 이는 재개했을 때 돌아올 위치를 나타낸다.
3. Continuation 추가
fun test(continuation: Continuation) { val continuation = object : ContinuationImpl { ... } when (continuation.label) { 0 -> { println("Hello") delay(1000L) } 1 -> { println("World!") } } }
- suspend 키워드가 사라지고 파라미터로 continuation이 추가
- continuation 인터페이스의 구현체인 Continuation object가 생성
4. 실행 후 중단(Suspend)
fun test(continuation: Continuation) { val continuation = object : ContinuationImpl { ... } when (continuation.label) { 0 -> { println("Hello") continuation.label = 1 // 중단 이후 라벨 1로 재개 if (delay(1000, continuation) == COROUTINE_SUSPEND) { return COROUTINE_SUSPEND } } 1 -> { println("World!") return Unit } } }
- label 값으로 실행 후 다음 값으로 값을 바꾼다.
- suspend function은 COROUTINE_SUSPEND를 반환할 수 있다.
- 이는 현재 코루틴이 중단 상태임을 나타낸다.
- 이를 리턴해줌으로써 함수가 끝난 것 같은 효과를 주어 스레드를 블로킹하지 않을 수 있다
5. 재개 (Resume)
fun test(continuation: Continuation) { val continuation = object : ContinuationImpl { override fun resumeWith() { test(continuation = this) } } when (continuation.label) { 0 -> { println("Hello") continuation.label = 1 // 중단 이후 라벨 1로 재개 delay(1000L) } 1 -> { println("World!") return Unit } } }
- Continuation의 resumeWith() 함수를 호출해서 중단된 이후 블록부터 코루틴을 재개
🌟 성능 - Thread vs Coroutine
Coroutine은 light-weight thread 라고 불린다.
- Coroutine은 스레드 안에서 실행된다.
- 같은 스레드에 10, 100개의 Coroutine이 있을 수 있다.
- 하지만, 한 시점에 여러 Coroutine이 실행된다는 것은 아니다.
- Coroutine은 메인 스레드 뿐 아니라 다른 백그라운드 스레드에서도 동작할 수 있다.
❗ 동시성 프로그래밍에서 한 시점에서는 하나의 Coroutine만 실행된다.
즉, Coroutine은 스레드 안에서 실행되고 Coroutine을 활용하여 Main-safe 하게 할 수 있다.
✋ 스레드는 프로세스에 종속이 되지만, Coroutine은 특정 스레드에 종속되지 않는다는 점이 큰 특징이다.
- Coroutine은 resume 될 때마다 다른 스레드에서 실행될 수 있다.
💡 작업이 스레드에 종속되지 않는다?
- 스레드를 blocking 하지 않으면서 작업의 실행을 잠시 중단할 수 있게 한다.
- 스레드 B가 스레드 A의 작업이 끝날 때 까지 대기해야 하는 작업을 한다면 이를 잠시 중단하고 그동안 다른 작업을 할 수 있게 한다.
- 이 특징은 스레드를 blocking 하지 않아 더 빠른 연산이 가능하게 하고 메모리 사용량을 줄여 많은 동시성 작업을 수행할 수 있게 한다.
🧑💻 성능 차이 코드 예제
fun main() = runBlocking { val amount = 10_000 // 실행할 작업 수 println("✅ Comparing performance with $amount tasks") println("🛫 ${Thread.activeCount()} threads active at the start") // 코루틴 실행 시간 측정 val coroutineTime = measureTimeMillis { createCoroutines(amount) } println("⏰ Running time for createCoroutines is $coroutineTime ms") // 스레드 실행 시간 측정 val threadTime = measureTimeMillis { createThreads(amount) } println("⏰ Running time for createThreads is $threadTime ms") } suspend fun createCoroutines(amount: Int) = coroutineScope { val jobs = ArrayList<Job>() for (i in 1..amount) { jobs += launch(Dispatchers.Default) { delay(1000L) } } println("🛬 ${Thread.activeCount()} threads active after createCoroutines") jobs.forEach { it.join() } } fun createThreads(amount: Int) { val jobs = ArrayList<Thread>() for (i in 1..amount) { jobs += Thread { Thread.sleep(1000L) }.also { it.start() } } println("🛬 ${Thread.activeCount()} threads active after createThreads") jobs.forEach { it.join() } }
1. amount = 100
2. amount = 1000
3. amount = 10000
📕 생성된 스레드 수
Coroutines Threads amount = 100 13 thread 113 thread amount = 1000 13 thread 1013 thread amount = 10000 13 thread Out Of Memory 📕 소요된 실행 시간
Coroutines Threads amount = 100 1012 ms 1017 ms amount = 1000 1054 ms 1059 ms amount = 10000 1098 ms Out Of Memory ❗ 메모리 측면에서 큰 차이가 보인다.
- Coroutine을 사용하면 불 필요한 blocking을 하지 않아 스레드 생성을 줄일 수 있기 때문
📍 참고 자료
'🔖 Kotlin' 카테고리의 다른 글
😎 Infix Function (0) 2024.12.30 널 안정성 (0) 2024.12.29 🦥 lateinit 과 lazy (0) 2024.12.29 Unit / Nothing (0) 2024.11.25 Gradle Kotlin DSL (2) 2024.11.21