Kotlin/코루틴

공식문서로 알아보는 Coroutines basics

Junuuu 2023. 12. 3. 00:01
728x90

개요

Kotlin 공식문서를 보며 코루틴의 개념에 대해 알아보고 실습도 진행해보고자 합니다.

 

 

Gradle 의존성 추가

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")

 

 

첫 번째 실습

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch는 새로운 코루틴을 만들어냄
        delay(1000L) // 특정 시간동안 코루틴을 일시 중단, 기본 스레드는 차단되지 않고 다른 코루틴이 실행될 수 있음
        println("World!") // print after delay
    }
    println("Hello") // main 코루틴은 이전에 생성된 코루틴이 일시중단되는 것과 상관없이 실행됨
}

//결과
Hello
World!

 

만약 순차적으로 코드가 실행된다면 1초 후에 World! Hello가 출력되어야 할 것입니다.

하지만 launch 부분을 새로운 코루틴을 만들어서 실행하는 것 자체가 스레드와 개념적으로 유사합니다.

물론 코루틴은 특정 스레드에 할당되진 않으며 하나의 스레드에서 실행을 중단했다가 다른 스레드에서 다시 시작할 수 있습니다.

 

launch는 코루틴의 builder역할을 수행합니다.

코루틴은 독립적으로 동작하기 때문에 Hello가 먼저 출력되었습니다.

 

delay는 특정 시간 동안 코루틴을 일시 중단시키지만 스레드를 block 시키진 않습니다, 따라서 해당 스레드에 다른 코루틴이 실행될 수 있습니다.

 

runBlokcing도 코루틴의 builder로 launch 키워드는 CoroutineScope안에서만 실행될 수 있습니다. 내부의 모든 코루틴이 실행을 완료할 때까지 blocking 시킵니다.

애플리케이션의 최상위인 main에서 사용하는 경우가 많으며 실제 코드 내부에서는 거의 사용되지 않습니다.

 

"코루틴은 순차적으로 실행되지만 서로 다른 코루틴끼리는 순차적으로 실행되지 않는구나"

 

yield와 delay로 조금 더 명확하게 확인해 보기

여러 코루틴은 중단과 재개가 반복됩니다.

yield에 대해 간단하게 설명하자면 현재 코루틴 디스패처의 스레드(또는 스레드 풀)를 가능한 경우 동일한 디스패처의 다른 코루틴이 실행하도록 넘깁니다.

 

@Component
class CoroutinesLecture1: ApplicationRunner {
    override fun run(args: ApplicationArguments?) {
        runBlocking {
            println("START")
            launch {
                newRoutine()
            }
//            yield()
            println("END")
        }
    }

    suspend fun newRoutine(){
        val num1 = 1
        val num2 = 2
//        delay(50)
        println("${num1 + num2}")
    }
}

 

위의 코루틴은 어떻게 동작할까요?

START -> END -> 3 순으로 결과가 출력됩니다.

 launch도 새로운 코루틴을 생성하기 때문에 2개의 코루틴이 돌아가고 있으면서 경합하게 됩니다.

 

그러면 START -> 3 -> END 가 출력될 수도 있지 않을까요?

맞습니다. 코루틴의 내부 스케쥴러 로직이나, OS 등에 의해 결정될 것 같습니다.

우리가 흔히 코루틴을 경량 쓰레드라고 부르는 것처럼 쓰레드도 t1, t2가 2개 실행되고 있으면 무엇이 먼저 실행될지는 OS 등이 결정하는 것과 유사하게 이해했습니다.

 

@Component
class CoroutinesLecture1: ApplicationRunner {
    override fun run(args: ApplicationArguments?) {
        runBlocking {
            println("START")
            launch {
                newRoutine()
            }
            yield()
            println("END")
        }
    }

    suspend fun newRoutine(){
        val num1 = 1
        val num2 = 2
//        delay(50)
        println("${num1 + num2}")
    }
}

이번에는 yield()로 명시적으로 다른 코루틴이 실행하도록 넘겨주었습니다.

START -> 3  -> END 가 출력됩니다.

 

@Component
class CoroutinesLecture1: ApplicationRunner {
    override fun run(args: ApplicationArguments?) {
        runBlocking {
            println("START")
            launch {
                newRoutine()
            }
            yield()
            println("END")
        }
    }

    suspend fun newRoutine(){
        val num1 = 1
        val num2 = 2
        delay(50)
        println("${num1 + num2}")
    }
}

이번에는 yield()로 명시적으로 다른 코루틴이 넘겨받았지만 해당 코루틴에서 delay(50)을 호출합니다.

해당 루틴은 일시중단되어 runBlokcing 라인의 루틴이 재개가 되어 START -> END -> 3의 결과로 출력됩니다.

 

Structured concurrency

코루틴은 구조화된 동시성 원칙을 따른다고 하는데 이게 무슨 의미일까요?

코루틴은 lifetime을 제한하는 CoroutineScope 내에서만 실행될 수 있습니다.

실제 애플리케이션에서는 많은 코루틴이 실행되고, Structured concurrency를 통해 코루틴이 손실되지 않고 누수되지 않도록 보장합니다.

 

한마디로 상위(부모) 코루틴이 하위(자식) 코루틴이 완료될 때까지 기다려줍니다.

 

 

함수 추출

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

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

 

launch { ... } 내부에 있는 코드를 함수로 추출하려면 어떻게 해야 할까요?

suspend 키워드를 활용하여 함수를 만들어 추출할 수 있습니다.

 

public suspend fun delay(timeMillis: Long)

suspend는 유예하다, 중단하다는 의미를 가지고 있으며 앞서 활용했던 delay 메서드로 내부적으로는 suspend로 구현되어 있습니다.

 

 

coroutineScope에서 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

 

1초 뒤 World 1

2초 뒤 World 2가 출력됩니다.

그리고 coroutineScope가 종료되고 난 뒤에 Done이 호출됩니다.

 

courotineScope와 runBlocking의 차이점

coroutineScope는 non-blokcing으로 스레드를 차단하지 않으며 해당 스코프 내에서 실행된 모든 자식 코루틴이 완료될 때까지 호출된 코루틴을 일시 중단합니다.

 

반면 runBlocking은 그 안에 포함된 서브 코루틴이 완료될 때까지 현재 스레드를 차단합니다.

 

명시적으로 작업 기다리기

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")

launch라는 코루틴 빌더는 Job 객체를 반환합니다.

해당 Job의 join메서드를 호출하면 해당 코루틴이 완료될 때까지 대기합니다.

 

 

코루틴은 스레드에 비해 자원을 효율적으로 사용한다

Coroutines를 사용하면 JVM의 스레드보다 자원을 덜 사용합니다.

예를 들어 50,000개의 스레드를 생성하는 경우 메모리가 부족하여 Out Of Memory 예외가 발생할 수 있지만 코루틴의 경우에는 메모리를 거의 사용하지 않습니다.