Kotlin/코루틴

공식문서로 알아보는 Composing suspending functions

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

개요

Kotlin 공식문서를 보며 코루틴의 suspend function 작성방법에 대해 알아보면서 실습해보고자 합니다.

 

 

기본적으로 순차적으로 실행된다

val time = measureTimeMillis {
    val one = doSomethingUsefulOne()
    val two = doSomethingUsefulTwo()
    println("The answer is ${one + two}")
}
println("Completed in $time ms")

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

//결과
The answer is 42
Completed in 2017 ms

 

suspend 키워드를 활용하여 2가지 메서드를 만들어 보고 1초의 delay를 주었습니다.

순차적으로 호출되면서 13 + 29의 결과인 42를 만들어냅니다.

코루틴이라고 해서 마법처럼 concurrent 하게 동작할 것 같지만 기본적으로는 순차적으로 실행되기 때문에 2초 이상의 수행시간이 소요됩니다.

 

async 키워드를 활용하여 concurrent하게 만들기

val time = measureTimeMillis {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")

//결과
The answer is 42
Completed in 1022 ms

단순하게 async 키워드를 붙여주면 이제 1초 정도의 시간만 소요됩니다.

이때 두 함수간에는 의존성이 없어야 합니다.

 

launch와 async는 유사하지만 luanch는 job을 반환하고 async는 Deffered를 반환합니다.

await() 메서드를 통해 최종 결과를 가져올 때까지 기다릴 수 있으며 Deffered는 job을 상속받고 있지만 비동기에 대한 결과를 나중에 제공하겠다고 약속하는 가벼운 non-blokcing입니다.

 

Lazy 한 async 만들기

val time = measureTimeMillis {
    val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
    val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
    // some computation
    one.start() // start the first one
    delay(1000L)
    two.start() // start the second one
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")

시작 매개변수를 CoroutineStart.LAZY로 설정하여 비동기를 지연 상태로 만들고, start나 await에 의해 요구될 때 코루틴이 시작됩니다.

one과 two 사이에 1초 동안 delay를 주면 기존에 1초가 소요되던 완료시간이 2초로 증가하게 됩니다.

 

GlobalScope를 활용하여 어디서나 호출할 수 있도록 사용하지 말기

// The result type of somethingUsefulOneAsync is Deferred<Int>
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

// The result type of somethingUsefulTwoAsync is Deferred<Int>
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

코루틴에서는 이런 스타일을 강력하게 권장하지 않습니다.

suspend를 붙이지 않고 어디서나 해당 함수를 호출할 수 있게 만들어낼 수 있습니다.

 

하지만 GlobalScope이기 때문에 예외가 발생하더라도 백그라운드에서 코루틴이 여전히 수행될 수 있습니다.

 

Structured concurrecy 활용하기

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    try {
        failedConcurrentSum()
    } catch(e: ArithmeticException) {
        println("Computation failed with ArithmeticException")
    }
}

suspend fun failedConcurrentSum(): Int = coroutineScope {
    val one = async<Int> { 
        try {
            delay(Long.MAX_VALUE) // Emulates very long computation
            42
        } finally {
            println("First child was cancelled")
        }
    }
    val two = async<Int> { 
        println("Second child throws an exception")
        throw ArithmeticException()
    }
    one.await() + two.await()
}

//결과
Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException

 

GlobalScope가 아닌 coroutineScope를 활용합니다.

이렇게 되면 two를 반환하는 async 코루틴에서 예외가 발생하더라도 예외가 전파되어 다른 코루틴이 종료됩니다.