Beeeam

코루틴 공식 문서 읽기 (Composing suspending functions) 본문

Kotlin

코루틴 공식 문서 읽기 (Composing suspending functions)

Beamjun 2023. 3. 13. 22:08

https://kotlinlang.org/docs/composing-suspending-functions.html

 

Composing suspending functions | Kotlin

 

kotlinlang.org

 

이 섹션에서는 suspend 함수의 구성에 다양한 접근법들을 다룬다.

Sequential by default

코루틴의 코드는 일반적인 코드와 마찬가지로 순차적 호출을 사용한다.

fun main() = runBlocking<Unit> {
    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 2010 ms

코루틴 내에서 순차적으로 함수를 호출하여 각 변수에 값을 할당한 다음에 결과 값을 출력하는 것을 볼 수 있다. 그리고 결과의 총 소요 시간을 보면 함수가 순차적으로 호출 되었기 때문에 각각의 함수의 소모 시간의 합이라는 것을 알 수 있다.

 

Concurrent using async

만약 앞의 예의 두 suspend 함수가 서로 의존성이 없고 둘 중 그냥 빨리 처리되는 것부터 값을 받아오고자 하면 async { … } 빌더를 사용하면 된다.

비동기적으로 실행하게 되면 위의 코드를 더 빠르게 처리할 수 있다.

async은 launch 처럼 새로운 코루틴을 생성하고, 실행하지만 launch는 job을 반환하고 결과 값을 전달하지 않지만, 비동기는 Deferred를 반환하는 차이점이 있다.

Deferred: job을 확장하는 인터페이스로 만들어져서 job의 모든 특성을 가지고 있다. async을 사용하고 await()을 사용하면 Deferred를 반환한다.

fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    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 1025 ms

 

먼저 눈에 띄는 점은 시간이 이전 예제보다 2배 감소 된 것이다. 이는 두 개의 코루틴이 동시에 실행 되기 때문이다.

이전 예제와 코드가 달라진 것은 doSomethingUseful…() 함수를 async { … } 빌더 내부에서 사용하는 것이다. 이 때문에 값을 반환 받을 때도 await() 함수를 사용하는 것을 볼 수 있다.

 

Lazily started async

async에 start 인자로 CoroutineStart.LAZY를 할당하여 시작을 지연할 수 있다. 이렇게 해서 지연된 함수는 await() 함수에서 결과가 필요하거나, 작업의 시작 함수가 호출된 경우에만 코루틴을 시작한다.

fun main() = runBlocking<Unit> {
    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
        two.start() // start the second one
        println("The answer is ${one.await() + two.await()}")
    }
    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 1042 ms

이전 예제와 출력 되는 결과는 다른 것이 없다. 하지만 실행되는 과정은 많이 다르다.

val 변수 = async( … } 에서 코루틴 함수가 바로 실행 되는 것이 아니라 값이 필요한 밑의 변수.start() 에서 시작이 된다.

만약 start 함수를 주석 처리 하고 실행 했다면 await 함수에서 두 함수가 실행이 됐을 것이고, 이로 인해 처음 예제에서 처럼 총 소요 시간은 각각의 함수의 소요 시간의 합이 된다.

 

Async-style functions

위의 예제의 doSomethingUseful… 함수를 비동기 스타일의 함수로 정의 할 수 있다. 이는 GlobalScope의 async { … } 빌더로 구현이 가능하다.

이 함수들은 비동기 연산들만 시작하고, …Async 접미사를 사용하여 결과를 얻기 위해서는 Deferred을 사용해야 함을 강조한다.

@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

위와 같은 형식으로 비동기 스타일 함수를 선언할 수 있다. 그런데 GlobalScope는 non-trivial 방법으로 역효과가 날 수 있는 API라서 @OptIn(DelicateCoroutinesApi::class)와 함께 사용해야 한다.

 

이렇게 선언된 xxxAsync 함수는 suspend 함수가 아니라서 어디서나 사용이 가능하다. 하지만 이 함수를 실행하면 항상 비동기로 실행됨을 기억해야 한다.

fun main() {
    val time = measureTimeMillis {
        // we can initiate async actions outside of a coroutine
        val one = somethingUsefulOneAsync()
        val two = somethingUsefulTwoAsync()
        // but waiting for a result must involve either suspending or blocking.
        // here we use `runBlocking { ... }` to block the main thread while waiting for the result
        runBlocking {
            println("The answer is ${one.await() + two.await()}")
        }
    }
    println("Completed in $time ms")
}

위의 코드는 비동기 스타일 함수를 코루틴이 아닌 곳에서 사용하는 예시이다.

근데 만약에 val one = somethingUsefulOneAsync() 와 one.await() 사이에서 오류가 있으면 실행 중 예외가 발생할 것 이다.

전역 에러 핸들러는 이 예외를 감지하고 개발자에게 예외를 보고 할 수 있다. 하지만 초기화 작업이 중단 되었음에도 불구하고 somethingUsefulOneAsync 가 백그라운드에서 여전히 실행 중인 문제가 생길 수 있다.

이러한 문제는 구조화된 동시성에서는 발생하지 않는다.

 

Structured concurrency with async

Concurrent using async 예제에서 doSomethingUseful… 함수들을 동시에 실행하고 해당 함수들의 반환 값을 합으로 반환하는 함수를 만들어보았다.

async { … } 빌더는 코루틴 스코프의 확장 함수로 정의되어 있기 때문에 이 빌더를 코루틴 스코프 안에서 호출이 가능할 수 있다. 이는 coroutineScope() 함수가 제공한다.

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()
}

main 함수에서 failedConcurrentSum() 함수를 실행하면 호출한 함수 내부의 await() 함수를 만나면서 두 개의 비동기 연산이 동시에 실행된다. 그런데 one에서는 지연 함수 때문에 값을 반환하지 못하고, two의 값이 먼저 출력된다. 그리고 예외를 발생하면서 one의 finally 문이 실행되어 문장을 출력하고, 그리고 이 예외가 main 함수에도 전달이 되서 catch 문 안의 코드가 실행되어 문장이 출력된다.