오늘도 더 나은 코드를 작성하였습니까?

Composing suspending functions (suspend 함수 구성하기) 본문

Coroutine/coroutineBasic

Composing suspending functions (suspend 함수 구성하기)

hik14 2022. 7. 5. 19:00

Sequential by default (기분 순차적 구성)

두개의 유용한 작업을 하는 suspend 함수가 있다고 가정하자.

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
}

위 2개의 함수의 결과 값을 합친 값을 필요로한다면 어떻게 해야되나? 

 

보통 코드와 마찬가지로 코루틴의 코드는 기본적으로 순차적이기 때문에 일반 순차 호출을 사용합니다.

다음 예제에서는 두 가지 suspend fun 을 모두 실행하는 데 걸리는 총 시간을 측정.

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

Concurrent using async (비동기를 이용하여 동시성 이룩)

doSomethingUsefulOne과 doSomethingUsefulTwo의 호출에 있어 종속성이 없다면,

두 가지를 동시에 수행하여 더 빨리 답을 얻으려면 어떻게 해야 합니까? 여기에서 async가 도움이 됩니다.

 

개념적으로 async는 launch와 동일하다.

- 코루틴 빌더이다.

- 다른 코루틴과 동시에 작동하는 경량 스레드인 별도의 코루틴을 시작 시킬수 있다.

 

가장 큰 차이점은 launch은 job을 반환하고 결과 값을 전달하지 않는 반면, async는 Deferred를  반환한다는 것

 

Deferred란?  나중에 결과를 제공하겠다는 약속을 나타내는 경량의 non-blocking future

 .await()를 사용하여 최종 결과를 얻을 수 있지만 Deferred도 작업이므로 필요한 경우 취소할 수 있습니다.

두 개의 코루틴이 동시에 실행되기 때문에 두 배 빠릅니다. 코루틴과의 동시성은 항상 명시적입니다.

 

Lazily started async (지연되어 시작하는 비동기 작업)

매개변수를 start = CoroutineStart.LAZY로 설정하여 async{ ... } 를 지연시킬 수 있습니다.

결과가 await에 의해 요구되거나 job의 start() 함수가 호출될 때만 코루틴을 시작합니다. 

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

start() 함수를 통해 코루틴의 시작을 개발자가 컨트롤 할 수 있다. 또 await()을 이용하여 둘중 어떤 코루틴이 먼저 종료되던 상관없이 결과값을 받아 서 출력한다.

 

만약, 각각의 코루틴에서 start를 먼저 호출하지 않고 println에서 await를 호출하면 await가 코루틴 실행을 시작하고 완료될 때까지 기다리기 때문에 순차적 동작으로 이어집니다. 이는 LAZY 대한 옳바른 사례가 아니다.

 

async(start = CoroutineStart.LAZY)의 사용할때,  결과 값를 도출하는데 있어서 suspend function이 포함되는 경우 standard lazy function와 같다.

 

Async-style functions (비동기식 함수)

구조적 동시성을 opt-out하기 위해 GlobalScope 사용하는 async 코루틴 빌더를 사용하여 doSomethingUsefulOne 및 doSomethingUsefulTwo를 비동기적으로 호출하는 비동기 스타일 함수를 정의할 수 있습니다.

 

이러한 함수의 이름은 "...Async" 접미사로 지정하여 비동기 계산만 시작하고 결과를 얻기 위해 deferred value을 사용해야 한다는 사실을 강조한다.

 

GlobalScope는 일반적이지 않은 방식으로 역효과를 일으킬 수 있는 섬세한 API이기 때문에, @OptIn(DelicateCoroutinesApi::class)과 함께 GlobalScope를 사용하도록 명시적으로 opt-In해야 합니다.

 

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

xxxAsync 함수는 suspend fun이 아니다, 그래서 이 함수는 일반함수기 때문에 어디에서나 사용할 수 있습니다.

그러나 이러한 사용은 항상 호출 코드와 함께 해당 job의 비동기식(여기서는 동시 실행을 의미함) 실행을 의미합니다.

// note that we don't have `runBlocking` to the right of `main` in this example
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")
}

비동기 함수가 있는 이 프로그래밍 스타일은 다른 프로그래밍 언어에서 널리 사용되는 스타일이다. 하지만, 이러한 스타일을 Kotlin 코루틴과 함께 사용하는 것은 매우 권장하지 않습니다.

 

val one = somethingUsefulOneAsync()과 one.await() 사이에 코드에 논리적 오류가 있다고 가정해보자.

프로그램에서 exception이 발생하고 프로그램에서 수행하던 작업이 중단된다.

 

일반적으로 global error handler는 exception를 포착하고 개발자를 위해 오류를 기록하고 보고할 수 있지만,  오류를 처리, 보고하는 처리기가 없다면,  프로그램은 다른 작업을 계속 수행할 것이다.

 

시작한 작업이 중단되었음에도 불구하고 백그라운드에서 계속 실행 중인 somethingUsefulOneAsync가 있습니다.

이 문제는 아래 섹션에 표시된 것처럼 구조화된 동시성에서는 발생하지 않습니다.

Structured concurrency with async

Concurrent using async 예제를 사용하여 doSomethingUsefulOne과 doSomethingUsefulTwo를 동시에 수행하고 결과의 합계를 반환하는 함수를 추출해 보자

 

async{ ... } 코루틴 빌더는 CoroutineScope의 extention으로 정의되어 있으므로 scope를 포함하고있다.

scope는 coroutineScope 함수가 제공하는 것.

suspend fun concurrentSum(): Int = coroutineScope {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    one.await() + two.await()
}

concurrentSum 함수의 코드 내에서 문제가 발생하고 예외가 발생하면 해당 범위에서 시작된 모든 코루틴이 취소됩니다.

또한 두개의 job역시 동시에 실행됩니다.

 

 

취소는 항상 코루틴 hierarchy에 따라서 통해 전파됩니다.

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

자식 중 하나(즉, two)가 실패하면 one async{ ... } 비동기와 대기 중인 부모가 모두 취소되는것에 유의해야된다.

'Coroutine > coroutineBasic' 카테고리의 다른 글

Coroutine scope  (0) 2022.07.07
Coroutine context and dispatchers  (0) 2022.07.06
Cancellation and timeouts  (0) 2022.07.05
coroutine basics(코루틴의 기초 공식문서 번역 설명)  (0) 2022.06.29