ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 공식 가이드 문서로 Coroutine 공부하기 - [03. Composing Suspending Functions]
    개발/Kotlin 2020. 11. 7. 04:23

     

    [ 03. 일시중지 함수 구성 ]

     


     

    일시중지 함수(suspending function)의 구성에 대한 다양한 접근방법을 다룹니다.

     

     

    # 기본적으로 순차적입니다


    다른 곳에 정의 된 두 개의 Suspending function이 원격 서비스 호출 또는 계산과 같은 유용한 작업을 수행한다고 가정합니다. 이들은 유용한 척 할 뿐이지, 실제로 각각의 코드는 해당 예제를 위해 단지 1초간 지연될 뿐입니다.

     

    - code [전체코드 확인하기] :

    suspend fun doSomethingUsefulOne(): Int {
        delay(1000L) // 여기에서 어떤 유용한 일을 하고 있는 것처럼 동작합니다
        return 13
    }
    
    suspend fun doSomethingUsefulTwo(): Int {
        delay(1000L) // 여기에서 어떤 유용한 일을 하고 있는 것처럼 동작합니다
        return 29
    }

     

    먼저 doSomethingUsefulOne을 호출하고 doSomethingUsefulTwo를 호출한 후, 이들의 결과의 합계 계산을 순차적으로 호출해야하는 경우 어떻게 해야하나요? 실제로 우리는 첫 번째 함수의 결과를 사용하여 두 번째 함수의 호출 여부를 결정하거나 실행 방법을 결정할 때 이 작업을 수행합니다.

     

    코루틴의 코드는 일반적인 코드와 마찬가지로, 기본적으로 순차적이기 때문에  일반적인 순차 호출을 사용합니다. 아래 예제는 두 suspending function을 실행하는데 걸리는 총 시간을 측정하여 보여줍니다.

     

    - code [전체코드 확인하기] :

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

     

    - result :

    The answer is 42
    Completed in 2017 ms

     

     

     

     

    # 비동기를 사용한 동시(Concurrent)


    doSomethingUsefulOnedoSomethingUsefulTwo호출 간의 종속성이 없고 두 가지를 동시에 수행함으로써 더 빠르게 답을 얻고자 한다면 어떻게 해야 할까요? async 키워드를 통해 해결할 수 있습니다.

     

    개념적으로, asynclaunch와 같습니다.  이들은 다른 모든 코루틴과 동시에 작동하는 경량 스레드인 별도의 코루틴을 실행시킵니다. 다만 launchJob을 리턴하고 어떠한 결과값도 전달하지 않는 반면, async는 추후 결과값을 제공하겠다는 약속을 나타내는 경량 non-blocking future인 Deferred를 리턴한다는 것입니다. Deferred 값에 .await()을 사용하여 최종 결과값을 얻을 수 있지만 DeferredJob이므로 필요한 경우 취소(cancel) 할 수 있습니다.

     

    - code [전체코드 확인하기] :

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

     

    - result :

    The answer is 42
    Completed in 1017 ms

     

    두 개의 코루틴이 동시에 실행되므로, 위의 예제 코드가 두 배 빠릅니다. 코루틴과의 동시성은 항상 명시적입니다.

     

     

     

     

    # 느리게 시작되는 비동기


    선택적으로, asyncstart 파라미터를 CoroutineStart.LAZY로 설정하여 지연시킬 수 있습니다. 해당 모드에서는 await에 의해 결과 값이 요구되거나 Jobstart 함수를 호출하였을 때에만 코루틴을 시작시킵니다. 아래의 예제를 실행하십시오.

     

    - code [전체코드 확인하기] :

    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
        val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
    
        one.start() // 첫 번째 코루틴 실행
        two.start() // 두 번째 코루틴 실행
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")

     

    - result :

    The answer is 42
    Completed in 1017 ms

     

    따라서, 여기에서는 두 개의 코루틴이 정의 되었지만 이전 예제코드처럼 실행되지 않습니다. 그러나 start를 호출하여 실행을 정확히 언제 시작하여야 하는지 지정해야할 경우, 제어권은 프로그래머에게 주어집니다. 먼저 one 실행(start) 후, two 실행(start)한 다음 개별 코루틴이 종료되기를 기다립니다.

     

    개별 코루틴에서 start를 먼저 실행시키지 않고 println 내에서 await을 호출하였을 경우, 순차적으로 동작하게 되는데, await은 코루틴 실행을 시작시키고 완료될 때까지 대기하므로 이는 laziness의 의도된 사용 사례가 아닙니다. async(start = CoroutineStart.LAZY)의 사용사례는 값 계산에 suspending function이 포함되는 경우, 표준 lazy 함수를 대체합니다.

     

     

     

     

    # 비동기 스타일 함수


    명시적인 GlobalScope 참조가 있는 async 코루틴 빌더를 사용하여 doSomethingUsefulOnedoSomethingUsefulTwo를 비동기적으로 호출하는 비동기 스타일 함수를 정의할 수 있습니다. 이러한 함수의 이름에 "...Async" 접미사를 붙여, 비동기 연산만을 시작하고 결과값을 얻기 위해 Deferred Value를 사용해야한다는 사실을 강조할 수 있습니다.

     

    - code [전체코드 확인하기] : 

    // somethingUsefulOneAsync의 리턴 타입은 Deferred<Int>
    fun somethingUsefulOneAsync() = GlobalScope.async {
        doSomethingUsefulOne()
    }
    
    // somethingUsefulTwoAsync의 리턴 타입은 Deferred<Int>
    fun somethingUsefulTwoAsync() = GlobalScope.async {
        doSomethingUsefulTwo()
    }

     

    이러한 xxxAsync 함수들은 suspending function아닙니다. 이 함수들은 어디에서나 사용할 수 있으나, 호출(invoking) 코드를 사용한 작업의 비동기 실행(여기에서는 동시(concurrent))을 의미합니다.

     

    - code [전체코드 확인하기] :

    // 해당 예제코드의 'main' 함수 우측에 'runBlocking'이 없습니다
    fun main() {
        val time = measureTimeMillis {
            // 코루틴 외부에서 비동기 작업을 시작시킬 수 있습니다
            val one = somethingUsefulOneAsync()
            val two = somethingUsefulTwoAsync()
            // 그러나 결과를 기다리는 것은 일시중지(suspending) 또는 blocking을 수반해야 합니다
            // 여기에서는 결과값을 대기하는 동안 `runBlocking { ... }`을 사용하여 main thread를 block 합니다
            runBlocking {
                println("The answer is ${one.await() + two.await()}")
            }
        }
        println("Completed in $time ms")
    }

     

    - result :

    The answer is 42
    Completed in 1118 ms

     

     


    비동기 함수가 있는 이 프로그래밍 스타일은 다른 프로그래밍 언어에서 널리 사용되는 스타일이므로, 여기에서는 설명용으로만 제공됩니다. 이러한 스타일을 Kotlin 코루틴과 함께 사용하는 것은 아래에 설명된 이유로 인해 권장되지 않습니다. 

     

    val one = somethingUsefulOneAsync()one.await() 사이에 로직에러가 있고 프로그램이 예외를 발생시키고 프로그램에서 수행되고 있던 작업이 중단될 경우, 어떻게 되는지 고려합니다. 일반적으로 Global 에러 핸들러가 해당 예외를 포착하고 개발자를 위해 오류를 기록하고 보고할 수 있지만 그렇지 않으면 프로그램이 다른 작업을 계속할 수 있습니다. 그러나 해당 예제에서는 해당 작업을 시작시킨 작업이 중단 되었음에도 불구하고 somethingUsefulOneAsync 가 백그라운드에서 여전히 실행되고 있습니다. 해당 문제는 아래 섹션에 표시된 것처럼 구조화(structure) 된 동시성(concurrency)에서는 발생하지 않습니다.

     

     

     

     

    # 비동기(Async)를 사용한 구조화(Structure) 된 동시성(Concurrency)


    Concurrent using async 예제를 사용하여 doSomethingUsefulOne doSomethingUsefulTwo를 동시에 수행하고 결과 값의 합계를 반환하는 함수를 추출해 보겠습니다. async 코루틴 빌더는 CoroutineScope의 extension으로 정의되어 있으므로, scope 내에 포함시켜야 하며 이것이 coroutineScope 함수가 제공하는 기능입니다.

     

    - code [전체코드 확인하기] :

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

     

    이렇게 하면 concurrentSum함수의 코드 내부에서 문제가 발생하여 예외가 발생할 때, 해당 scope에서 시작된 모든 코루틴이 취소됩니다.

     

    - code [전체코드 확인하기] :

    val time = measureTimeMillis {
        println("The answer is ${concurrentSum()}")
    }
    println("Completed in $time ms")

     

    - result :

    The answer is 42
    Completed in 1017 ms

     

     

    위의 main 함수의 출력값에서 알 수 있듯이, 두 작업을 동시에 실행합니다.

     

     

    취소는 항상 코루틴 계층 구조를 통해 전파됩니다.

     

    - code [전체코드 확인하기] :

    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) // 매우 긴 연산을 모방합니다
                42
            } finally {
                println("First child was cancelled")
            }
        }
        val two = async<Int> { 
            println("Second child throws an exception")
            throw ArithmeticException()
        }
        one.await() + two.await()
    }

     

    - result :

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

     

    하위항목 중 하나(즉, two)에서의 에러로 인해, 첫 번째 async 및 대기중인 상위항목이 취소되는 방법에 유의해야합니다. 

     

     

     


     

     

    Composing Suspending Functions - Kotlin Programming Language

     

    kotlinlang.org

     

     

    댓글

Designed by Tistory.