개요
•
일시 중단 연산이 실제로 어떻게 동작하는지 이해하는 것은 매우 중요하다.
•
컴파일러가 일시 중단 함수를 상태 머신(state-machine)으로 변환하는 방법과 스레드 스위칭이 어떻게 발생하고 예외가 전파되는지 분석
연속체 전달 스타일
suspend fun something(continuation: Continuation) {
println("A")
log.debug("B")
val a = something2(continuation) // suspend
// resume
val c = 5
// .. . .
// . . .
continuation.resume()
continuation.resumeWithException()
}
suspend fun something2(continuation: Continuation) {
//
continuation.resumeWithException()
}
Kotlin
복사
•
코드로 구현된 일시 중단 연산은 실제로는 연속체 전달 스타일(CPS; Continuation Passing Style)로 수행
•
이 패러다임은 호출되는 함수에 연속체를 보내는 것을 전재하며, 함수가 완료되는 대로 연속체를 호출(콜백이라 생각 가능)
◦
완료/오류에 따라 필요한 연속체 전달/호출
◦
이러한 내부 동작은 컴파일러가 수행하므로 일시 중단 함수의 시그니처는 코드와 컴파일 결과가 다름
또한 모든 일시 중단 함수는 상태 머신으로 변환되고, 상태를 저장하고 복구하며 한 번에 코드의 한 부분만 실행.
•
따라서 일시 중단 연산은 재개할 때 마다 중단된 위치에서 상태를 복구하고 실행을 재개한다.
•
CPS와 상태 머신이 결합하면 컴파일러는 다른 연산이 완료되기를 기다리는 동안 일시 중단 될 수 있는 연산을 구성할 수 있게 된다.
→ 상태 머신은 한 번에 코드의 한 부분만 실행
→ 실행이 완료되면 연속체를 호출해서 작업의 완료를 전달
연속체
•
모든 것은 일시 중단 연산의 빌딩 블록이라 볼 수 있는 연속체로부터 시작 (결과적으로는 일종의 패러다임)
→ 연속체는 코루틴을 재개할 수 있다는 점에서 매우 중요하다.
interface Continuation<in T> {
val context: CoroutineContext
fun resume(value: T)
fun resumeWithException(exception: Throwable)
}
Kotlin
복사
•
연속체는 확장된 콜백에 가까우며 다음과 같은 정보를 포함
◦
호출되는 컨텍스트에 대한 정보
◦
일시 중단 연산의 재개 callback
◦
일시 중단 연산의 예외 전파 callback
suspend 한정자
•
코틀린은 동시성을 지원하기 위해 가능한 언어에 변화를 작게 가져가려고 노력
•
대신, 코루틴 및 동시성의 지원에 따른 영향을 컴파일러/표준 라이브러리/코루틴 라이브러리에 포함
suspend fun something() {
val resultA = somethingA() // suspending fun
val resultB = sometinngB() // suspending fun
}
Kotlin
복사
•
코드에는 suspend만 붙이면 일시 중단 연산이 컴파일될 때 마다 바이트코드가 하나의 커다란 연속체가 됨
◦
위 코드의 경우 suspend fun something() 의 실행이 연속체를 통할 것으로 표현되며 내부의 일시중단 연산에 연속체를 전달
상태머신
•
컴파일러가 코드를 분석하면 일시 중단 함수가 상태머신으로 변환됨
•
상태머신으로 변환된 일시 중단 함수는 현재의 상태를 기초로 해서 매번 재개되는 다른 코드 부분을 실행
suspend fun something(continuation: Continuation) {
// label 0 -> execution
log.info("first execution")
fetchFromDb(continuation) // suspend
// label 1 -> resume
log.info("resume")
something() // not suspend
fetchFromCache(continuation) // suspend
// label 2 -> resume
/* .. */
}
Kotlin
복사
그럼 여기서 label은 어떻게 나타내는가? → 이 부분이 연속체가 주목받는 점
•
우선 컴파일러가 변환을 하는 것
•
각 label로 돌아가는 부분은 호출이 완료되면 어디로 돌아가라~는 연속체(콜백)을 이용
Appendix
suspend fun first() = "first"
suspend fun second() = 2
suspend fun third() = 3L
suspend fun wrap() {
println("step 1")
println("---Result1 ${first()}")
println("step 2")
println("---Result2 ${second()}")
println("step 3")
println("---Result3 ${third()}")
//
}
Kotlin
복사
suspend fun wrap(continuation: Continuation, result: /* .. */) {
label1: {
// initiate Continuation
// ...
}
label2: {
label3: {
when(continuation.label) {
0 -> {
// Step 1. first suspending
continuation.label = 1
break
}
1 -> {
// get Result or throw
break
}
2 -> {
// get Result or throw
break label3
}
3 -> {
// get Result or throw
break label2
}
else -> throw Exception(...)
}
// get Result or throw
continuation.label = 2
// Step 2. second suspending
}
// 두번째 suspending 결과 출력
continuation.label = 3
// Step 3. third suspending
}
// last code block
}
fun main() = runBlocking { continuation ->
var label: Int = 0
var result: /* .. */
invokeSuspend(result) {
when(label) {
0 -> {
label = 1
wrap(continuation, result)
// get Result or throw
}
1 -> {
// get Result or throw
}
}
}
}
Kotlin
복사