Programming Language/Kotlin

코루틴 기본

수연초이 2022. 12. 16. 03:44

코루틴이란?

코루틴이란 중단이 가능한 연산의 인스턴스이다. 실제 사용할 때는 스레드와 매우 다르지만, 이해를 위해서라면 가벼운 스레드라고 볼 수 있다.

서로 다른 부분의 코드 블럭을 동시에 수행할 수 있다는 점에서 스레드 개념과 유사하다. 하지만 코루틴은 특정 스레드에 연결된 것은 아니다. 하나의 스레드에서 코루틴의 실행이 중단되었더라도, 다른 스레드에서 코루틴이 동작할 수 있다.

코루틴 개념 정리

 

코루틴을 사용하기 위해서는 dependency를 추가해야한다

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
}

(참고: https://github.com/Kotlin/kotlinx.coroutines/blob/master/README.md#using-in-your-projects)

 

 

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

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
}

runBlocking : 코루틴 빌더. 비코루틴 영역인 `fun main()`과 `runBlocking{...}`  안의 코루틴 코드를 연결해준다. 현재 실행 중인 스레드(위 코드에서는 메인 스레드)는 runBlocking 내부에 있는 모든 코루틴 실행이 완료될 때까지 중단(blocking)된다. 스레드를 생성하고 중단하는 것은 비효율적이고 바람직하지 않은 값비싼 행위이기 때문에 최상위 수준에서 거의 사용되고, 실제 코드 내부에서는 잘 사용되지 않는다. 참고로 위 코드에서 runBlocking이 없다면 launch를 사용할 수 없다. launch는 CoroutineScope에 정의되어있기 때문이다.

launch : 코루틴 빌더. 나머지 코드 부분과 동시에 코루틴을 생성하며, 해당 코루틴은 독립적으로 실행된다. Hello가 먼저 출력되는 이유이다.

delay : suspending function(중단 함수)이다. 특정 시간 동안 코루틴을 중단한다. 코루틴이 중단되면, 해당 스레드는 중단되지 않고 다른 코루틴이 스레드에서 실행된다.

 

Structured Concurrency(구조화된 동시성)

코루틴은 Structured Concurrency 원리를 따른다. 새로운 코루틴은 특정 CoroutineScope(제한된 코루틴 수명)에서만 실행된다는 의미이다. 위 코드에서는 runBlocking에서 CoroutineScope를 생성하여 1초 후에 World!가 출력된 후 종료된다.

실제 어플리케이션에서 많은 코루틴이 생성될 수 있다. Structured Concurrency는 코루틴이 손실되거나 누출되지 않도록 보장해준다. 또한 코드 오류가 제대로 전달되어 손실되지 않음을 보장한다.

 

Suspending function

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

private suspend fun doWorld() { // suspending function
    delay(1000L)
    println("World!")
}

launch { } 내부의 코드를 별도의 함수로 추출하면 suspend라는 modifier가 붙는데, 해당 함수가 바로 suspending function이다. Suspending Function(중단 함수)는 코루틴 내부에서 사용할 수 있으며, 다른 중단 함수(위 코드에서는 delay())를 호출하여 코루틴을 중단할 수 있다. 

쉽게 말하자면 delay가 중단함수라, delay를 감싼 함수 doWorld에 suspend를 붙여줘야한다. 만약 suspend를 붙여주지 않으면 컴파일 오류가 나고, delay가 없으면 굳이 suspend를 선언할 필요가 없다.

 

Scope builder

CoroutineScope 빌더를 사용해서도 코루틴 범위를 지정할 수 있다. CoroutineScope 빌더는 코루틴 scope를 생성하고 모든 하위 항목이 완료될때까지 유지된다. runBlocking도 하위 항목(자식)이 완료될때까지 대기하므로 비슷해보이지만, runBlocking은 현재 스레드를 blocking하는 반면, coroutineScope는 일시 중단되어 스레드를 다른 곳에 사용할 수 있다는 차이가 있다. 이 차이로 인해 runBlocking은 일반 함수이고, coroutineScope는 suspend 함수이다.

다음과 같이 suspend 함수에서 coroutineScope을 사용할 수 있다. 

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

 

Scope builder & Concurrency

coroutineScope 빌더는 suspend 함수에서 여러 개의 동시성 작업을 수행할 때 사용할 수 있다. 아래 코드는 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")
}

1. launch{...} 블록 내의 코드는 동시에 수행된다.

2. 중단 작업이 없는 'Hello'가 출력된다.

3. 1초 후 'World 1'이 출력된다.

4. 2초 후 'World 2'가 출력된다.

5. doWorld의 coroutineScope는 2, 3 두가지 작업이 모두 완료된 후에 종료된다.

6. 'Done'이 출력된다.

 

An explicit job

코루틴 빌더인 launch는 Job 객체를 리턴하고 Job 객체가 완료될 때까지 명시적으로 대기할 때 사용할 수 있다.

val job = launch { // launch a new coroutine and keep a reference to its Job
    delay(1000L)
    println("World!")
}
println("Hello")
job.join() // 자식 코루틴이 완료될 때까지 대기. 해당 라인이 없다면 Hello 출력 -> Done 출력 -(1초 후)-> World! 출력
println("Done")

위의 코드에서는 자식 코루틴이 완료('World!' 출력)될때까지 대기한 후, 'Done'이 출력된다

 

코루틴은 가볍다

코루틴은 JVM 스레드보다 리소스를 덜 사용한다. JVM에서 사용할 수 있는 메모리를 스레드가 거의 다 쓰는 코드를, 코루틴을 사용한다면 그러지 않아도 된다.

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

위 코드는 각각 다른 십만개의 코루틴을 실행하고, 각 코루틴은 5초 대기 후 '.'를 출력한다. 하지만 메모리를 거의 사용하지 않는다. 

만약 동일한 프로그램인데 스레드를 사용하는 경우(runBlocking 제거하고 launch를 스레드로 대체, delay는 Thread.sleep으로 대체) 메모리를 매우 많이 사용하고 out-of-memory 오류가 발생한다.

fun main() {
    repeat(100_000) {
        Thread { // 스레드 사용
            Thread.sleep(5000L)
            print(".")
        }.start()
    }
}

OutOfMemoryError 오류 발생:

[0.297s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 2048k, guardsize: 16k, detached.
[0.297s][warning][os,thread] Failed to start the native thread for java.lang.Thread "Thread-4073"
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
	at java.base/java.lang.Thread.start0(Native Method)
	at java.base/java.lang.Thread.start(Thread.java:802)
	at CoroutineKt.main(Coroutine.kt:6)
	at CoroutineKt.main(Coroutine.kt)
[1.030s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 2048k, guardsize: 16k, detached.
Error occurred during initialization of VM
java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached

Process finished with exit code 1

 

출처: https://kotlinlang.org/docs/coroutines-basics.html#coroutines-are-light-weight