기본 코루틴(Coroutine Basics)

케니스·2022년 12월 31일
0

첫번째 코루틴(Your first coroutine)

코루틴은 일시 중단 가능한 계산의 인스턴스입니다. 코드의 블록들이 다른 코드 들과 동시에 동작해야 한다는 점에서는 개념적으로 스레드와 유사합니다. 하지만 코루틴은

나머지 코드와 동시에 작동하는 코드들이 하나의 블럭에서 실행해야 코드의 블럭이 실행될 때 동시적으로 실행할 수 있습니다. 그러나 코루틴은 특정한 스레드에 바운드 되지 않고 한 스레드에서 일시 중단하고 다른 스레드에서 다시 재개할 수 있습니다.

코루틴은 경량 스레드라고 생각할 수 있습니다. 하지만 실제 우리가 사용하는 스레드와는 다르게 만드는 중요한 요소들이 있습니다.

다음 코드를 실행하여 첫번째 코루틴 작업을 확인해봅니다.

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
}

// result
Hello
World!

다음과 같은 결과가 나오는 걸 볼 수 있습니다.

한번 이 코드가 무엇인지 해부해보겠습니다

launch는 코루틴 빌더입니다. launch는 새로운 코루틴을 실행함과 동시에 나머지 코드들을 독립적으로 계속 작업합니다. 그것이 Hello가 첫번째로 프린트된 이유입니다.

delay 는 특별한 일시중단 함수입니다. 코루틴을 특정 시간동안 일시 중단합니다. 코루틴을 일시중단해도 기본스레드가 차단되지는 않으며 다른 코루틴들도 기본스레드를 사용하여 코루틴을 실행하할 수 있습니다.

runBlockingfun main() 과 같은 기본적인 코루틴이 아닌 세계와 runBlocking { ... } 중괄호 안에 있는 코루틴 코드들을 연결하는 코루틴 빌더입니다. runBlocking 코드를 작성해보면 IDE에서 중괄호 바로 뒤에 CoroutineScope라는 힌트가 표시됩니다.

launch는 CoroutineScope에서만 선언이 가능하기 때문에 만약 깜빡하고 runBlocking을 코드에서 제거한다면 launch를 호출할 때 에러가 발생합니다.

Unresolved reference: launch

runBlocking 은 의미는 runBlocking { ... } 내부의 모든 코루틴들이 실행이 완료 될 때까지 이를 실행하는 스레드(메인스레드라고 가정)는 차단된다는 것을 의미합니다. 스레드는 차단하는 것은 스레드의 비용이 비싸기 때문에 비효율적이지만 실제코드에서는 앱의 최상위 레벨에서 runBlcoking 자주 사용되는 것을 볼 수 있습니다.

구조화된 동시성(Structured concurrency)

코루틴 구조화된 동시성을 원칙을 따릅니다. 즉, 구조화된 동시성이란 새로운 코루틴은 오직 코루틴의 수명을 제한하는 특정한 CoroutineScope에서만 실행될 수 있다는 것을 의미합니다. 위의 예제에서는 runBlocking 해당하는 스코프를 설정하고 World!는 몇초뒤에 프린트 된 것을 볼 수 있습니다.

실제 앱에서는 많은 양의 코루틴을 실행해야합니다. 구조화된 동시성은 손실이나 릭을 발생하지 않게 보장합니다. 외부 범위는 모든 하위 코루틴이 완료될 때까지 완료할 수 없습니다. 또한 구조화된 동시성은은 코드에서 발생한 모든 에러가 올바르게 보고되고 손실이 발생하지 않게 합니다.


함수 추출 리팩토링(Extract function refactoring)

launch { ... } 내부의 코드 블록을 별도의 함수로 추출해보겠습니다. 만약 코드에서 "함수 추출"이라는 리팩토링을 수행한다면 suspend 수정자와 함께 새로운 함수를 만들 수 있습니다. 이건 당신의 첫번째 중단 함수 입니다. 중단 함수는 코루틴 내부에서 일반적인 함수처럼 사용합니다 하지만 일반적인 함수와 다른 점은 코루틴을 일시중지하기 위해 다른 코루틴 함수의 기능들(deplay 함수 같은) 추가 기능들이 존재한다는 것입니다.

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

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

스코프 빌더(Scope builder)

코루틴스코프 빌더를 사용하여 다른 빌더에서 제공되는 코루틴 스코프외에도 자신만의 스코프를 선언할 수 있습니다. 그리고 코루틴 스코프를 생성한 후 해당 스코프내의 생성된 자식 작업들이 완료되기 전까지는 스코프가 완료되지 않습니다.

fun main() = runBlocking {
    doWorld()
}

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

runBlocking 그리고 coroutineScope 빌더들은 둘다 바디와 모든 자식들이 작업이 완료되기까지 기다리는 것으로 유사하게 보이지만 가장 큰 차이점은 runBlocking 함수가 대기를 위해 현재 스레드를 차단한다면 coroutineScope는 단순히 일시중단하고 다른 용도를 위해 기본 스레드를 해제합니다. 그 차이 때문에 runBlocking은 일반 함수이고 coroutineScope는 정지함수 입니다.

coroutineScope를 어떠한 중단함수로서 사용할 수 있습니다. 예를 들어 Hello와 World의 동시적인 로그 출력을 suspend fun doWorld() 로 옮길 수 있습니다.


스코프 빌더 그리고 동시성(Scope builder and concurrency)

coroutineScope 빌더는 여러 동시 작업을 수행하기 위해 정지 함수 내에서 사용할 수 있습니다

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

launch { ... } 블록 내부에 두 개의 코드들은 동시적으로 실행되며 World 1이 먼저 프린트 된 후 1초 후에 World 2의 프린트가 시작되고 2초 뒤에 프린트합니다. coroutineScope doWorld

코루틴 스코프에 있는 doWorld의 함수는 두 개의 작업이 모두 완료된 후에만 완료됩니다. 그래서 doWorld는 모든 작업이 완료된 후 Done 문자열이 인쇄되도록 허용합니다

Hello
World 1
World 2
Done

명시적 작업(An explicit job)

launch 코루틴 빌더는 Job 객체를 리턴합니다. 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") 
// result
Hello
World!
Done

코루틴은 가볍습니다(Coroutines are light-weight)

코루틴은 JVM 스레드에서 리소스 집약이 더 적습니다. 스레드를 사용할 때 JVM의 가용 메모리를 소진하는 코드는 리소 제한에 도달하지 않고 콜튄을 사용하여 표현할 수 있습니다.

스레드에서 JVM의 가용 메모리를 소진하는 코드는 코루틴을 사용하는것으로 리소스 제한에 도달하지 않을 수 있습니다. 예를들어 다음 코드는 100000개의 서로 다른 코루틴을 시작하여 각각 5초를 기다린 다음 마침표('.')를 프린트하는 코드가 있습니다. 이 코드는 매우 적은 메모리를 사용합니다.

import kotlinx.coroutines.*

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

해당 코드를 스레드에서 비교하고 싶으신 경우 runBlockinglaunchthread로 교체하고 delay 함수도 Thread.sleep으로 교체해줍니다. 만약 해당 코드를 실행한다면 많은 메모리를 소비하여 OOM(out-of-memoery)에러가 발생할 수 있습니다.

profile
노력하는 개발자입니다.

0개의 댓글