Programming Language/Kotlin

[Kotlin] Dive into Concept of Kotlin Coroutine and CoroutineContext. (suspend keyword)

개발왕 금골드 2023. 7. 1. 18:07
반응형
  • 소개
    • CoroutineScope
    • CoroutineContext
    • CoroutineBuilder
  • suspend keyword
    • 예제
    • Structured concurrency
    • Coroutine continuation

소개

코루틴은 일시 중단이 가능한 계산의 인스턴스이다. 안드로이드의 UI 스레드 실행을 중단시키지 않고 다른 스레드에서 계산을 실행할 수 있다는 점에서 개념적으로 스레드와 유사하다. 그러나 코루틴은 특정 스레드에 바인딩되지 않고, 한 스레드에서 실행을 일시 중단하고 다른 스레드에서 다시 시작할 수 있다. 말이 어려울 수 있다. 중요한 점은 코루틴은 일시 중단이 가능하다는 것이다.
 
Coroutine을 구성하는 요소로 CoroutineScope, CoroutineContext, CoroutineBuilder 등이 있다.

CoroutineScope

코루틴의 실행 범위를 생성한다. (예: GlobalScope, ViewModelScope, CoroutineScope 등)

CoroutineContext

코루틴 실행을 어떻게 할 것인지에 대한 요소들을 결정한다.

  • Dispatcher (예: Dispatchers.Main, Dispatchers.IO, Dispatchers.Default 등)
  • Job (예: Job, Deffered 등)

CoroutineBuilder

코루틴을 실행할 수 있게 한다. (예: launch, async, withContext 등)
 

suspend 키워드

suspend 키워드가 붙은 함수들은 코틀린 컴파일러가 컴파일할 때 CPS(Continuation-Passing-Style)로 사용될 수 있도록 Continuation 파라미터가 함수 마지막 파라미터로 추가된다. 또한, suspend 키워드가 붙은 함수는 코루틴이나 다른 suspend 안에서만 호출할 수 있다는 제약이 생김과 동시에 코루틴이 제공하는 다른 suspend 함수를 사용할 수 있게 된다. suspend 키워드는 앞에서 말한 코루틴이 중단 가능하다를 실현해 주는 키워드이다. 코루틴 안에서 suspend 함수가 호출되면 호출 시점에서의 실행 정보들을 Continuation 객체로 만들어 캐시에 저장하고 실행이 재개되었을 때 실행 정보를 바탕으로 이어서 실행된다.

예제

fun main() = runBlocking {
  someTask1()
  someTask2()
}

suspend fun someTask1() {
  // Do Something
  delay(10_000) // delay도 suspend 함수이다.
  // Do Something
}

suspend fun someTask2() {
  // Do Something
  delay(10_000) // delay도 suspend 함수이다.
  // Do Something
}

main 함수 안에 runBlocking 이라는 CoroutineBuilder를 생성하였다. SomeTask1이라는 suspend 함수를 실행하는데 이 안에 delay라는 suspend 함수가 실행될 때 실행 정보를 저장한다. delay 함수는 일정 시간 실행을 멈추는 함수인데, SomeTask1이 멈춰있는 동안 다른 suspend 함수에게 실행 기회가 제공된다. SomeTask2는 실행 기회를 얻고 실행되며, 마찬가지로 delay 함수가 실행될 때 잠시 중단된다. SomeTask1의 delay 시간이 끝나면 남아 있는 suspend 함수 SomeTask1이 실행 기회를 얻고 delay 뒤에 있는 코드가 실행된다. 이렇게 하나의 스레드 안에서 실행 시간을 나눠 여러 작업이 실행될 수 있다.
 
 

 
 
일반 함수 호출을 운영체제에서 호출 스택으로 관리한다. suspend 함수는 코루틴 프레임워크에서 CPS 방식으로 호출 정보를 스택 형태로 유지한다. 호출 스택의 마지막 suspend 함수가 실행을 종료하면 결과 값이 직전 suspend 함수로 전파되며 직전 함수를 resume한다. 만약, 스택 안에 어떤 함수가 예외를 발생시키면 예외 정보를 최초 호출 함수까지 전달한다.
 
 

Structured concurrency

코루틴의 Structured concurrency는 수명을 제한하는 특정 코루틴 범위에서만 새로운 코루틴을 시작할 수 있음을 의미한다. 모든 하위 코루틴이 완료될 때까지 외부 범위를 완료할 수 없으며, suspend 함수 스택 안에서 어떤 함수가 예외를 발생시켰을 때 예외 정보를 최초 호출 함수까지 전달하여 해당 범위의 모든 코루틴을 종료시킨다. Structured concurrency는 코루틴이 손실되지 않고 누출되지 않도록 보장한다. 또한, 모든 오류가 올바르게 보고되고 손실되지 않는다.

Coroutine Continuation

코루틴 안에서 suspend 함수가 실행되면 이전 실행 정보를 Continuation 객체로 저장한다. 실행 정보들을 저장하고 가지고 있기 때문에 suspend 함수가 종료되면 이전 실행 코드로 다시 돌아올 수 있다. Continuation은 코루틴 실행 흐름이 중단되었다는 것(suspend)을 저장하고 어떻게 재개(resume) 해야 하는지 알고 있는 객체이다. Continuation은 연쇄적으로 생성되며 저장된다. 이를 테면, Coroutine Caller의 Continuation이 제일 처음 생성되고 caller 안에 suspend 함수의 Continuation이 Caller’s Continuation에 추가적으로 저장되며, suspend 함수 자신도 해당 정보를 갖고 있는다. 이런 식으로 Stack처럼 Caller’s Continutation에 정보들이 쌓이고 어떤 함수를 resume 해야 하는지 알 수 있게 된다.

public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}

Continuation이 필요한 이유 중 한 가지는 스레드이다. JVM은 스레드를 차단하지 않고 함수 내부에서 기다릴 수 없다. 그렇기 때문에 Continuation 객체가 전달되면서 suspend 함수 안에서 COROUTINE_SUSPENDED 라는 특수한 값을 즉시 리턴한다. 특수한 값이 리턴되면 해당 로직이 실행되고 있는 스레드를 Free 상태로 만들면서 이 스레드는 중단되지 않고 다른 일을 처리할 수 있게 되는 것이다. 예를 들어, 스레드 A가 Free 상태가 되면 그 자리를 스레드 B로 대체하여 suspend 함수는 스레드 B에서 실행되고 스레드 A는 다른 일을 한다.
 
 

 
하지만 같은 Dispatcher 안에서 실행될 수도 있고 특정 Dispatcher를 설정하지 않는다면 부모와 같은 Dispatcher로 설정된다. 이런 경우에 Coroutine Caller의 코드와 suspend function A의 코드 블록은 사실 병렬로 실행되지 않는다. 이러한 경우는 결국 같은 스레드에서 실행되는 것이고 일반 함수와 스케줄링의 차이가 발생한다.
 

 
위 예제에서 block C가 먼저 스케줄링 되는스케줄링되는 이유는 suspend 함수가 COROUTINE_SUSPENDED 특수 값을 실행 즉시 리턴하기 때문이다. (물론, Code block에 따라, 반드시 저 순서대로 스케줄링되는 것은 아니다.) Caller 안에 속해 있는 suspend 함수는 Caller의 Continuation에 접근할 수 있다.
 


참고자료 :
https://myungpyo.medium.com/reading-coroutine-official-guide-thoroughly-part-0-20176d431e9d

 

Reading Coroutine official guide thoroughly — Part 0

요즘 안드로이드 앱 개발자들은 주개발 언어가 Java 에서 Kotlin 으로 많이 전환 되고 있다고 생각합니다. iOS 가 objective-c 에서 swift 로 전환할 때 처럼 안드로이드 SDK 공식 가이드에서도 Kotlin 과 Java

myungpyo.medium.com

https://kotlinlang.org/docs/coroutines-basics.html#extract-function-refactoring

 

Coroutines basics | Kotlin

 

kotlinlang.org

https://june0122.github.io/2021/06/09/coroutines-under-the-hood/

 

[Kotlin] Coroutine - 코루틴의 내부 구현

코루틴은 디컴파일되면 일반 코드일 뿐이다. Continuation Passing Style(CPS, 연속 전달 방식) 이라는 형태로 동작하며, 결과를 호출자에게 직접 반환하는 대신 Continuation으로 결과를 전달한다. Continuation

june0122.github.io

https://devroach.tistory.com/163

 

Coroutine Series-2) Continuation

Continuation Coroutine 에서 꼭 알아야 하는 부분 중 하나이지만 생소하지 않은 개념중 하나이다. Continuation 이란 말 자체가 생소해서 정의를 설명하기 보단, Continuation 이란 개념이 왜 만들어지게 됬는

devroach.tistory.com

https://stackoverflow.com/questions/71221947/why-can-you-run-a-kotlin-coroutine-on-the-main-thread

 

Why can you run a Kotlin coroutine on the main thread?

I am having trouble understanding why this piece of code can work properly: override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) launch(Dispatchers.Main)...

stackoverflow.com

https://stackoverflow.com/questions/73679497/how-does-continuation-work-in-kotlin-coroutine

 

How does Continuation work in Kotlin Coroutine?

I am studying about CPS. I was wondering how does it work. Object createPost( Token token, Item item, Continuation<Post> const){...} interface Continuation<in T> { val

stackoverflow.com

 
 
 
 

반응형