[Android] 코루틴 Under the hood

 

이번시간에는 코루틴이 내부적으로 어떻게 동작하는지 확인해보겠습니다

 

[Continuation Passing Style]

CPS == Callbacks

  
  //Kotlin
  suspend fun createPost(token : Token, item : Item) : Post { _ } 
  
  // Java/JVM
  Object createPost(Token token, Item item, Continuation<Post> cont) { _ } 
  

작성한 코루틴이 컴파일 될때 내부적으로 JVM에서 Byte 코드로 변환되면서 Continuation이 하나가 더 생기게 됩니다. (CPS)

 

 

 

[Labels]

  
  //Kotlin
  suspend fun postItem(item : Item) {
  // LABLE 0
 	val token = requestToken()
  // LABEL 1    	
    val post = createPost(token, item)
  // LABEL 2    
    processPost(post)
  }

  // Java/JVM
  suspend fun postItem(item : Item) {
   	  switch(label){
        case 0:
      	    val token = requestToken()
        case 1:
            val post = createPost(token, item)
        case 2:
            processPost(post)
      }
  }
  

suspend 함수가 붙게되면 컴파일될때 LABEL이 찍힙니다

중단지점과 재개되는 지점이 필요하기 때문에 LABEL 작업을 코틀린 컴파일러가 내부적으로 하게 됩니다

내부적으로는 switch/case 문으로 바뀌어 순차적으로 실행하게 됩니다

 

 

LABEL이 완성되고 나면 CPS 로 변하게 됩니다

  
  fun postItem(item : Item, cont : Continuation){
	  val sm = object : CoroutineImpl { .. }
	  switch(sm.label){
    	case 0:
            requestToken(sm) //state machine == continuation
        case 1:
            createPost(token, item, sm)
   		case 2:
        	processPost(post)
    }
  }

suspend function을 호출할 때마다 Continuation을 넘겨줍니다

Continuation은 callback 인터페이스 같은것이라고 합니다

Continuation은 어떤 정보값을 가진 형태로 계속 패싱이 되면서 코루틴이 내부적으로 동작하게 됩니다

 

 

 

  
  fun postItem(item : Item, cont : Continuation){
	  val sm = cont as? ThisSM ?: object : ThisSM {
    	  fun resume(_) {
        	  postItem(null, this)
          }
      }
	  switch(sm.label){
    	  case 0:
        	  sm.item = item
              sm.label = 1
              requestToken(sm)
          case 1:
              createPost(token, item, sm)
   		  ...
      }
  }
  

 

각 case 마다 연산이 끝났을때 continuation에 resume을 호출하게 됩니다

resume은 자기 자신을 다시 불러줍니다

그래서 label을 하나씩 늘려 다음 case를 실행시켜줍니다

 

 

 

[CPS simulation] 

  
  GlovalScope.launch {
  	val userData = fetchUserData()
    val userCache = cacheUserData(userData)
    updateTextView(userCache)
  }
  

밑의 코드는 위의 코루틴을 작성했을때 내부적으로 어떻게 동작되는지 알아볼수 있는 코드입니다

 

  
  fun main() {
      println("[in] main")
      myCoroutine(MyContinuation()) //메인을 실행하면 한번 불림 // 내부적으로 CPS
      println("\n[out] main")
  }

  fun myCoroutine(cont: MyContinuation) {
      when(cont.label) {	//LABEL 처리된 suspending point
          0 -> {
              println("\nmyCoroutine(), label: ${cont.label}")
              cont.label = 1
              fetchUserData(cont)
          }
          1 -> {
              println("\nmyCoroutine(), label: ${cont.label}")
              val userData = cont.result
              cont.label = 2
              cacheUserData(userData, cont)
          }
          2 -> {
              println("\nmyCoroutine(), label: ${cont.label}")
              val userCache = cont.result
              updateTextView(userCache)
          }
      } 
  }

  fun fetchUserData(cont: MyContinuation) {
      println("fetchUserData(), called")
      val result = "[서버에서 받은 사용자 정보]"
      println("fetchUserData(), 작업완료: $result")
      
    cont.resumeWith(Result.success(result)) //myCoroutine를 다시 부르면서 다음 LABEL로 넘어감
  }

  fun cacheUserData(user: String, cont: MyContinuation) {
      println("cacheUserData(), called")
      val result = "[캐쉬함 $user]"
      println("cacheUserData(), 작업완료: $result")
      cont.resumeWith(Result.success(result))
  }

  fun updateTextView(user: String) {
      println("updateTextView(), called")
      println("updateTextView(), 작업완료: [텍스트 뷰에 출력 $user]")
  }

  class MyContinuation(override val context: CoroutineContext = EmptyCoroutineContext)
      : Continuation<String> {

      var label = 0
      var result = ""

      override fun resumeWith(result: Result<String>) {
          this.result = result.getOrThrow()
          println("Continuation.resumeWith()")
          myCoroutine(this)
      }
  }
  

 

 

 

 

 

 

 

 

references :

kotlinlang.org/docs/reference/coroutines/coroutines-guide.html

www.youtube.com/channel/UCJeARDV434voq3IxRTBfzLw

 

 

댓글