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.