[Kotlin] Coroutines basics

코루틴 공식 문서를 번역하고 내용을 조금 변경하거나 내용을 추가한 게시글입니다. 잘못된 번역이 있을 수 있습니다. 참고한 공식 문서 바로가기


코루틴은 suspend 가능한 computation 인스턴스 입니다. 코루틴 block 안의 코드가 그 외의 코드와 동시에 작동한다는 점에서 개념적으로는 thread와 유사합니다. 그러나, 코루틴은 특정 스레드에 묶여 있지 않습니다. 예를 들어 코루틴은 A 스레드에서 중단(suspend)되고 B 스레드에서 재개될 수 있습니다.

코루틴은 경량 스레드라고 생각될 수 있습니다. 하지만 실제로 사용할 때 스레드와 중요한 차이점이 많이 있습니다.

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는 coroutine builder입니다.
코드의 나머지 부분과 동시에 독립적으로 작동하는 새로운 코루틴을 실행합니다. 독립적으로 작동하기 때문에 Hello가 먼저 출력됩니다.

delaysuspending function입니다. 코루틴을 특정 시간동안 중단시킵니다.
코루틴을 중단시켜도 스레드는 block되지 않습니다. 다른 코루틴의 실행을 허용하고 실행된 코루틴이 해당 스레드를 사용합니다.

runBlocking은 coroutine builder입니다.
runBlocking안에 있는 코루틴 코드를 non-coroutine world인 fun main()와연결합니다. 또한 { } 안은 CoroutineScope입니다.
launch의 경우 CoroutineScope안에서만 선언할 수 있습니다.

runBlocking 이름은 runBlocking을 실행하는 스레드가 runBlocking이 호출되는 기간 동안 block됨을 의미합니다. (runBlocking { ... }안의 모든 코루틴이 실행 완료될 때까지 block 됩니다.) runBlocking은 보통 application의 최상단에서 사용되고 실제 코드 안에서는 거의 사용하지 않는 것을 확인할 수 있습니다. 스레드는 expensive resources이고 이러한 스레드들을 blocking하는 것은 비효율적이고 바람직하지 않기 때문입니다.


Structured concurrency

새로운 코루틴은 특정한 CoroutineScope안에서 실행되어야 한다는 structured concurrency 원칙을 따릅니다. 특정한 CoroutineScope는 코루틴의 생명주기의 범위를 정합니다. 위 예제 코드에서 runBlocking이 runBlocking에 해당하는 범위를 지정한 것을 알 수 있습니다. 따라서 위 예제 코드는 World!가 출력된 이후에야 종료됩니다.

실제 앱에서 많은 코루틴을 실행합니다. Structured concurrency는 코루틴이 lost, leak됨을 방지합니다. outer scope는 모든 자식 코루틴이 완료되기 전까지 완료될 수 없습니다. Structured concurrency는 또한 코드 에러가 적절하게 보고 되고 lost되지 않게합니다.


Extract function refactoring

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

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

launch안의 코드를 별개의 함수로 분리해봅시다. 일반적인 함수와 다르게 suspend modifier가 붙은 것을 확인할 수 있습니다. 이를 suspending function이라 부릅니다. suspending function은 일반적인 함수와 마찬가지로 코루틴 내부에서 사용할 수 있습니다. suspending function의 특징은 다른 suspending functions함수(delay)를 사용할 수 있다는 것입니다.


Scope builder

runBlocking 말고도 다른 coroutineScope builder를 통해 coroutine scope를 제공할 수 있습니다. 이러한 coroutineScope builder는 실행된 자식(children)이 완료될 때까지 종료되지 않습니다.

runBlockingcoroutineScope는 서로 자식 코루틴이 종료될 때까지 기다린다는 점에서 유사합니다. 차이점은 runBlocking은 종료될 때까지 현재 thread를 멈추게(block)하는 반면 coroutineScope는 단순히 중단(suspend)하며 block하지 않고 thread를 다른 usage로 release합니다. 또한 runBlocking은 일반 함수이고 coroutineScope는 suspending 함수입니다.

모든 suspending function에서 coroutineScope를 사용할 수 있습니다. 단, suspend function은 무조건 CoroutineScope안에서 실행되야합니다.


Scope builder and concurrency

여러 개의 동시 작업을 위해 coroutineScope buildersuspending function에 사용할 수 있습니다. 동시에 발생하는 2개의 코루틴을 실행해봅시다.

// 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")
}
Hello
World 1
World 2
Done <br>

launch { ... }안의 코드는 동시에 실행됩니다. 1초 후에 World 1을 출력하고 2초 후에 World 2를 출력합니다. doWorld 안의 coroutineScope는 두 개의 launch가 종료된 이후에 끝납니다. 따라서 doWorld가 return(종료)된 이후에 Done이 출력됩니다.

coroutineScope 자체가 코루틴을 실행해주는거라 생각했는데 아니었다. 정말 범위만 지정해주는구나 … 예상 했던 실행 결과는 Hello -> Done -> World 1 -> World 2 였다.


An explicit job

launch coroutine builder는 Job 객체를 리턴합니다. Job객체는 실행된 coroutine을 다룰 수 있으며 coroutine의 종료를 기다릴 수 있습니다.

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


Coroutines are light-weight

코루틴은 JVM threads보다 자원을 덜 사용합니다. thread를 사용하여 JVM의 가용 메모리를 고갈시키는 코드는 코루틴을 사용하여 리소스 제한 없이 실행될 수 있습니다. 즉, 스레드를 사용하면 메모리 부족이 생기는 코드를 코루틴에서는 사용할 수 있습니다.
다음 코드는 100000개의 코루틴을 실행하지만 메모리를 거의 사용하지 않습니다.

import kotlinx.coroutines.*

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

위 코드를 threads를 사용한다면 (runBlocking을 지우고 launchthread로 바꾸고 delay 대신 Thread.sleep를 사용하는 방식으로) out-of-memory error가 발생합니다.

Categories:

Updated:

Leave a comment