소개

9장_코틀린의 동시성 내부

개요

일시 중단 연산이 실제로 어떻게 동작하는지 이해하는 것은 매우 중요하다.
컴파일러가 일시 중단 함수를 상태 머신(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
복사