Post

Kotlin Coroutine. Part01. 코틀린 코루틴 이해하기

Part01. 코틀린 코루틴 이해하기

코루틴의 동작 과정

  • kotlin coroutine 에서는 이런 CPS 로 각 subroutine 들을 제어한다.
  • subroutine 은 caller 의 context 로 돌아가는 대신 정의된 continuation 에 의해 제어( kotlin coroutine 은 state machine 을 통해서 subroutine 의 동작을 제어)되게 된다.

Kotlin 컴파일러가 suspend 키워드를 만나면 어떻게 동작할까?

1. CPS (continuation-passing style)

  • CPS란 호출되는 함수에 Continuation을 전달하고, 각 함수의 작업이 완료되는 대로 전달받은 Continuation을 호출하는 패러다임을 의미한다. 즉 Continuation을 일종의 콜백으로 생각할 수 있다.
  • 일시 중단 함수는 바이트코드로 컴파일되면서 Continuation이 생성되어 CPS로 변환된다.
  • CPS에선 Continuation을 전달하면서, 현재 suspend된 부분에서의 resume을 가능하게 해준다.
1
2
3
4
5
6
7
8
9
10
11
suspend fun postItem(item: Item): Post {
    val token = requestToken()
    val post = createPost(token, item)
    return processPost(post)
}
      
fun postItem(item: Item, cont: Continuation<Post>) { 
    val token = requestToken()
    val post = createPost(token, item)
    cont.resume(processPost(post))
}

2. State machine - Suspension Points를 기준으로 코드 블록이 구분

  • Kotlin 컴파일러는 함수가 내부적으로 Suspention Point(중단 지점과 재개 지점)을 식별하고, 이 지점으로 코드를 분리한다. 분리된 지점들은 각각의 label로 인식된다.
1
2
3
4
5
6
7
8
fun postItem(item: Item, cont: Continuation<Post>) { 
    // LABEL 0
    val token = requestToken()
    // LABEL 1
    val post = createPost(token, item)
    // LABEL 2
    cont.resume(processPost(post))
}
1
2
3
4
5
6
7
8
9
10
fun postItem(item: Item, cont: Continuation<Post>) {
    switch (label) {
        case 0:
            val token = requestToken()
        case 1:
            val post = createPost(token, item)
        case 2:
            processPost(post)
    }
}

3. State machine - label과 각 suspend fun 내부 변수들이 관리

  • Kotlin 컴파일러는 postItem 함수 안에 continuationImpl 구현 객체 state machine 를 만든다.
  • state machine은 결국 Continuation이고, Continuation이 어떠한 정보값을 가진 형태로 Passing이 되면서 코루틴이 내부적으로 동작하게 되는 것이다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    fun postItem(item: Item, cont: Continuation<Post>) {
      val sm = cont as? ThisSM ?: object : ThisSM {
          fun resume() {
              postItem(null, this)
          }
      }
    
      switch (sm.label) {
          case 0:
              throwOnFailure(sm.result)
              sm.label = 1
              requestToken(sm)
          case 1:
              throwOnFailure(sm.result)
              sm.label = 2
              createPost(token, item, sm)
          
      }
    }
    

resumeWith는 어떻게 동작할까? (BaseContinuationImpl)

BaseContinuationImpl 클래스를 통해 resumeWith 의 동작 방식을 알아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
internal abstract class BaseContinuationImpl(
    public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {

    public final override fun resumeWith(result: Result<Any?>) {
        var current = this
        var param = result
        while (true) {
            probeCoroutineResumed(current)
            with(current) {
                val completion = completion!!
                val outcome: Result<Any?> =
                    try {
                        val outcome = invokeSuspend(param)
                        if (outcome === COROUTINE_SUSPENDED) return
                        Result.success(outcome)
                    } catch (exception: Throwable) {
                        Result.failure(exception)
                    }
                releaseIntercepted()
                if (completion is BaseContinuationImpl) {
                    current = completion
                    param = outcome
                } else {
                    completion.resumeWith(outcome)
                    return
                }
            }
        }
    }
    ...
}

resumeWith

  • current 를 this (Continuation) 로 설정하고, param 을 resumeWith 의 인자로 받은 result 로 설정한다.
  • 무한 반복을 진행하고 with(current) 를 통해 현재 continuation 의 스코프를 열어준다.
  • invokeSuspend(param) 을 통해 현재 continuation 의 invokeSuspend 함수를 실행한다.
  • 결과로 CoroutineSingletons.COROUTINE_SUSPEND 이 반환됐다면 현재 함수가 suspending 상태라는것을 의미하니 return 을 통해 더 이상 진행을 중지한다.
  • CoroutineSingletons.COROUTINE_SUSPEND 이 아닌 다른 값이 나왔다면 **결과에 따라 Result 로 래핑하여 outcome 변수로 설정해**주고 있고, `BaseContinuationImpl` 의 인자로 받은 `completion` 가 `BaseContinuationImpl` 타입일 경우 **current 로 업데이트, 그리고 outcome 을 param 으로 업데이트하여 루프**를 다시 돌린다.
This post is licensed under CC BY 4.0 by the author.