시배's Android

Kotlin Coroutines Deep Dive | 4장. 코루틴의 실제 구현 본문

Book/Kotlin Coroutines Deep Dive

Kotlin Coroutines Deep Dive | 4장. 코루틴의 실제 구현

si8ae 2024. 1. 18. 22:15
  • 중단 함수는 함수가 시작할 때와 중단 함수가 호출되었을 때 상태를 가진다는 점에서 상태 머신과 비슷합니다.
  • 컨티뉴에이션 객체는 상태를 나타내는 숫자와 로컬 데이터를 가지고 있습니다.
  • 함수의 컨티뉴에이션 객체가 이 함수를 부르는 다른 함수의 컨티뉴에이션 객체를 장삭합니다. 그 결과, 모든 컨티뉴에이션 객체는 실행을 재개하거나 재개된 함수를 완료할 때 사용되는 콜 스택으로 사용됩니다.

Continuation Passing Style 

suspend fun getUser() : User?
suspend fun setUser(user : User)

fun getUser(continuation : Continuation<*>) : Any?
fun setUser(user : User, continuation : Continuation<*>) : Any
  • 중단 함수 내부를 들여다 보면 원래 선언했던 형태와 반환 타입이 다라졌습니다.
  • 이는 중단 함수를 실행하는 도중에 중단되면 선언된 타입의 값을 반환하지 않을 수 있기 때문입니다.
  • User? 또는 COROUTINE_SUSPENDED를 반환할 수 있기 때문에 결과 타입이 User?와 Any의 가장 가까운 슈퍼타입인 Any?로 지정되었습니다.
  • 상태를 저장하기 위해 자신만의 컨티뉴에이션 객체가 필요합니다.
val continutation = 
	if (continuation is MyFunctionContinuation) continuation
    else MyFunctionContinuation(continuation)
fun myFunction(continuation : Continuation<Unit>) : Any {
	val continuation = continuation as? MyFunctionContinuation
    	?: MyFunctionContinuation(continuation)
        
    if(continuation.label == 0){
    	println("Before")
        continuation.label = 1
        if(delay(1000, continuation) == COROUTINE_SUSPENDED){
        	return COROUTINE_SUSPENDED
        }
    }
    if(continuation.label == 1){
    	println("After")
        return Unit
    }
    error("Impossible")
}

class MyFunctionContinuation(
	val completion : Continuation<Unit>
) : Continuation<Unit> {
	override val context : CoroutineContext
    	get() = completion.context
        
    var label = 0
    var result: Result<Any>? = null
    var counter = 0
    
    override fun resumeWith(result : Result<Unit>) {
    	this.result = result
        val res = try {
        	val r = myFunction(this)
            if(r == COROUTINE_SUSPENDED) return
            Result.Success(r as Unit)
        } catch (e : Throwable) {
        	Result.failure(e)
        }
        completion.resumeWith(res)
    }
}
  • delay에 의해 중단된 경우 COROUTINE_SUSPENED가 반환되며, myFunction은 COROUTINE_SUSPENED를 반환 합니다.

값을 받아 재개되는 함수

suspend fun printUser(token : String) {
	println("Before")
    val userId = getUserId(token)
    println("Got userId : $userId)
    val userName = getUserName(userId, token)
    println(User(userId, userName))
    println("After")
}
  • 함수가 값으로 재개되었다면 결과는 Result.Success(value)가 되며, 이 값을 얻어 사용할 수 있습니다.
fun printUser(
	token : String,
    continuation : Continuation<*>
) : Any {
	val continuation = continuation as? PrintUserContinuation
    	?: PrintUserContinuation(
        	continuation as Continuation<Unit>,
            token
        )
        
    val result : Result<Any>? = continuation.result
    var userId : String? = continuation.userId
    val userName : String
    
    if(continuation.label == 0) {
    	println("Before")
        continuation.label = 1
        val res = getUserId(token, continuation)
        if(res == COROUTINE_SUSPENDED) {
        	return COROUTINE_SUSPENDED
        }
        result = Result.success(res)
    }
    if(continuation.label == 1) {
    	userId = result!!.getOrThrow() as String
        println("Got userId : $userId")
        continuation.label = 2
        continuation.userId = userId
        val res = getUserName(userId, token, continuation)
        if(res == COROUTINE_SUSPENDED){
        	return COROUTINE_SUSPENDED
        }
   		result = Result.success(res)
    }
    if(continuation.label == 2) {
    	userName = result!!.getOrThrow() as String
        println(User(userId as String, userName))
        println("After")
        return Unit
    }
    error("Impossible")
}

class PrintUserContinuation(
	val completion : Continuation<Unit>,
    val token : String
) : Continuation<String> {
	override val context : CoroutineContext
    	get() = completion.context
        
    var label = 0
    var result : Result<Any>? = null
    var userId : String? = null
    
    override fun resumeWith(result : Result<String>){
    	this.result = result
        val res = try {
        	val r = printUser(token , this)
            if(r == COROUTINE_SUSPENDED) return
            Result.success(r as Unit)
        } catch(e : Throwable) {
        	Result.failure(e)
        }
        completion.resumeWith(res)
    }
}

콜스택

  • 함수 a가 b를 호출하면 가상 머신은 a의 상태와 b가 끝나면 실행이 될 지점을 어딘가에 저장해야 합니다. 이런 정보들은 모두 콜 스택이라는 자료 구조에 저장됩니다.
  • 코루틴을 중단하면 스레드를 반환해 콜 스택에 있는 정보가 사라질 것입니다.
  • 따라서 코루틴을 재개할 때 콜스택을 사용할 수 없습니다.
  • 대신 컨티뉴에이션 객체가 콜 스택의 역할을 대신합니다.
  • 컨티뉴에이션 객체는 중단이 되었을 때의 상태(label)와 함수의 지역 변수와 파라미터, 그리고 중단 함수를 호출한 함수가 재개될 위치 정보를 가지고 있습니다.
  • 컨티뉴에이션 객체가 재개될 때 각 컨티뉴에이션 객체는 자신이 담당하는 함수를 먼저 호출합니다.
  • 함수의 실행이 끝나면 자신을 호출한 함수의 컨티뉴에이션을 재개합니다.

실제 코드

  • 예외가 발생했을 때 더 나은 스택 트레이스 생성
  • 코루틴 중단 인터셉션
  • 사용하지 않는 변수를 제거하거나 테일콜 최적화하는 등의 다양한 단계에서의 최적화