본문 바로가기

안드로이드/Kotlin

[Kotlin] 코틀린 공식 문서 - Coroutines basics

개요

해당 게시글은 Coroutines basics | Kotlin (kotlinlang.org)을 번역한 게시글입니다.

 

Your first Coroutine

코루틴은 일시 중단 가능한 연산(suspendable computation)의 객체이다. 이는 다른 코드와 동시에 작동되는 코드 블록이 실행된다는 점에서 개념적으로 스레드와 유사하다. 그러나 코루틴은 특정 스레드에 바인딩되지 않는다. 한 스레드에서 실행을 일시 중지하고 다른 스레드에서 다시 시작할 수도 있다.

 

코루티는 경량 스레드로 생각될 수 있지만 스레드와 매우 다른 몇 가지 차이점이 있다.

 

아래는 첫번째 코루틴 코드이다.

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch a new coroutine and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello") // main coroutine continues while a previous one is delayed
}

위 결과로 아래를 확인할 수 있다:

출력 결과

Hello

World!

 

이 코드가 무엇을 하는지 분석해보자:

 

launch는 코루틴 빌더이다. 독립적으로 계속 작동하는 나머지 코드와 동시에 새로운 코루틴을 시작한다. Hello가 먼저 출력된 이유이다.

 

delay는 특별한 suspending function이다. 이는 코루틴을 특정 시간 동안 suspend(일시 중지)한다. 코루틴을 일시 중지 하는 것은 스레드를 block하지 않고 다른 코루틴이 기본 스레드를 실행하고 사용할 수 있다.

 

runBlocking도 또 다른 코루틴 빌더이다. runBlocking을 통해 일반적인 fun main()의 non-couroutine 코드와 runBlocking{...} 중괄호 안에  코루틴이 있는 코드를 잇는 역할을 한다. 만약 위 코드에서 runBlocking을 작성하는 것을 까먹거나 지운다면 아래와 launch를 실행할 때 아래와 같은 에러를 만나게 된다. launch는 오직 CorouineScope에서만 선언이 가능하기 때문이다.

Unresolved reference: launch

 

runBlocking이라는 이름의 의미는 runBlocking{...} 내부의 모든 코루틴이 실행을 완료할 때까지 이를 실행하는 스레드가 호출하는 동안 차단된다는 것을 의미한다. 스레드는 비싼 리소스이며 스레드를 차단하는 것은 비효율적이기 때문에 실제로는 runBlocking은 거의 사용되지 않는다.

 

Structured Concurrency

코루틴은 구조화된 동시성(structured concurrency) 라는 원칙을 따른다. 이는 새로운 코루틴은 코루틴의 수명을 제한하는 CoroutineScope 내에서만 실행될 수 있음을 의미한다. 위의 코드에서는 runBlocking을 통해 이에 상응하는 scope를 설정했고 이전 예제에서 World!가 1초 지연된 후에 출력된 후 종료된 이유이다.

 

실제 앱에서 매우 많은 코루틴을 실행할 것이다. 구조화된 동시성은 이런 코루틴들이 손실되거나 누수되지 않도록 돕는다. 외부 Scope는 모든 하위 코루틴이 완료될 때까지 완료될 수 없다. 또한 구조화된 동시성은 코드의 오류가 제대로 보고되고 손실되지 않도록 한다.

 

 

Extract Fuction refactoring

launch {...} 내부의 코드 블록을 다른 함수로 분리해보자. 이 코드에서 "Extract function"이라는 리팩토링을 수행할 때, suspend가 있는 새 함수가 생성된다. Suspending function은 코루틴 내부에서 일반적인 함수처럼 사용된다. 하지만 몇 가지 특징은 코루틴의 실행을 suspend(일시 중단)하기 위해 다른 일시 중단 함수(ex. delay)를 사용하여 코루틴의 실행을 중단할 수 있다.

fun main() = runBlocking { // this: CoroutineScope
    launch { doWorld() }
    println("Hello")
}

// this is your first suspending function
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

 

 

Scope Builder

다른 빌더에서 제공되는 CoroutineScope 외에도 coroutineScope 빌더를 활용하여 자체적으로 scope를 정의할 수 있다. 이는 coroutine scope를 만들고 자식 코루틴들이 완료되기 전까지 완료되지 않는다.

 

runBlocking과 coroutineScope 빌더는 내부의 자식 코루틴이 완료될 때 까지 기다린다는 점에서 유사해보인다. 하지만 중요한 차이점은 runBlocking은 현재 스레드를 차단하는 반면, coroutineScope는 일시중단하고 다른 용도로 기본 스레드를 놓아준다. 이러한 차이점으로 runBlocking은 일반 함수(regular fun)이고 coroutineScope는 일시 중단 함수(suspend fun)이다.

 

어느 일시 중단함수에서나 coroutineScope를 사용할 수 있다. 예를들어, Hello와 World의 병행 출력을 suspend fun doWorld()로 이동시킬 수 있다.

fun main() = runBlocking {
    doWorld()
}

suspend fun doWorld() = coroutineScope {  // this: CoroutineScope
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello")
}

 

 

Scope Builder and Concurrency

coroutineScope 빌더는 여러 동시 작업을 위해 어느 일시 중단 함수에서나 사용될 수 있다. doWorld() 내에서 두개의 코루틴을 동시 실행해보자.

// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking {
    doWorld()
    println("Done")
}

// Concurrently executes both sections
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
    launch {
        delay(2000L)
        println("World 2")
    }
    launch {
        delay(1000L)
        println("World 1")
    }
    println("Hello")
}

launch{..} 내의 두 코드는 동시에 실행된다. 첫 번째는 1초 후에 World1이 인쇄되고, 2초 후에는 World2가 인쇄된다. doWorld의 coroutineScope는 둘 다 완료된 후에만 완료되기 때문에 그 후에야 Done이 출력될 수 있다.

 

출력 결과

Hello

World 1

World 2

Done

 

An Explicit Job

launch 코루틴 빌더는 Job 객체를 반환한다. 이는 실행된 코루틴을 핸들링하고 코루틴의 완료를 명시적으로 기다리는데 활용할 수있다. 예를들어, 자식 코루틴의 완료를 기다리고 Done을 출력할 수 있다.

val job = launch { // launch a new coroutine and keep a reference to its Job
    delay(1000L)
    println("World!")
}
println("Hello")
job.join() // wait until child coroutine completes
println("Done") 

출력 결과

Hello

World !

Done

 

join()을 명시적으로 처리하지 않았따면 Hello, Done, World 순으로 출력될 것이다. 

 

 

Coroutines are light-weight

import kotlinx.coroutines.*

//sampleStart
fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(".")
        }
    }
}
//sampleEnd

이는 100K의 코루틴을 실행하고, 5초 후  각 코루틴은 dot(.)을 출력한다. 만약 이를 스레드로 처리하게 바꾼다면 Out-Of-Memory 에러가 발생할 것이다.

 

 

요약

  • 코루틴은 일시 중단 가능한 연산 객체이다.
  • 구조화된 동시성이라는 개념을 통해 코루틴의 수명을 제한하는 CoroutineScope 내에서만 생성가능하다.
  • CoroutineScope는 자식 스레드가 모두 완료되기 전까지 완료되지 않는다.