[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

     

     

    댓글