ABOUT ME

-

  • Kotlin) Coroutine 공식 가이드 번역 03 - Composing Suspending Functions
    Kotlin 2021. 1. 15. 20:54

    https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html


    기본적으로 순차적

    코루틴은 기본적으로 순차적으로 수행됩니다. 예제에서 공통으로 사용하기 위한 suspend 함수를 생성합니다.

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

    순차적으로 doSomethingUsefulOne() 과 doSomethingUsefulTwo()를 호출하고, 리턴값을 더하여 출력합니다.


    비동기를 사용한 동시

    위의 예제에서 두개의 함수는 서로 인과관계를 갖지 않습니다. 

    그래서 따로 동시에 동작한다면 더 빠른 결과를 얻을 수 있을까요?

     

    코루틴의 async를 사용하여 두개의 작업을 분리 & 동시에 진행시킬 수 있습니다. launch와 동일한 컨셉이지만 리턴 객체가 다릅니다.

    • launch -> 새 코루틴을 시작하고 호출자에게 결과를 반환하지 않습니다. "실행 후 삭제"로 간주되는 모든 작업은 launch를 사용하여 시작가능합니다. Job 오브젝트 리턴, 어떠한 결과값도 전달해 주지 않습니다.
    • async -> 새 코루틴을 시작하고 await라는 정지 함수로 결과를 반환하도록 허용합니다. Deferred<T> 반환, 결과를 나중에 제공하는 Promise, Deferred는 Job을 상속받아 구현되었기 때문에 상태제어 또한 가능.

    보통 일반 함수는 await를 호출할 수 없으므로 일반 함수에서 새 코루틴을 launch해야 합니다. async는 다른 코루틴 내부에서만 사용하거나 정지 함수 내에서 병렬 분해를 실행할 때 사용합니다.

    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

    두개의 함수가 동시에 진행되었기 때문에 시간이 반으로 줄었습니다.

     

    Suspend vs Deferred

    • 둘다 suspend 형태를 취합니다.
    • scope에 종속되는 suspend function은 cancel 요청 시 전부 같이 취소 되지만, 위와 같이 자신의 scope를 만들어서 사용하는 deferredcancel을 따로 호출해줘야 합니다. 즉 따로 관리해야 함으로 suspend function 사용하는 것을 권장합니다.
    • 둘다 안정한 코드입니다.

    느리게 시작되는 비동기

    async로 시작되는 코루틴은 블럭 내 코드의 수행을 지연시킬 수 있습니다.

    optional 파라미터인 start의 값을 CoroutineStart.LAZY로 주면 해당 작업을 바로 수행하지 않습니다.

    start()를 명시적으로 불러주거나 await()를 호출해야 됩니다.

     

    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

    위 코드는 명시적으로 one.start(), two.start()를 호출하여 async를 수행합니다. 그래서 동시에 진행된 시간이 출력됩니다.

    단, start()를 삭제해도 println 내부의 await()를 만나면 해당 블럭은 실행됩니다.

    await()는 실행이 완료될때까지 대기하기 때문에, one.await()가 완료되어야 two.await()가 동작되는 순차적 구조입니다.

     

    withContext

    • async와 동일한 역할을 하는 키워드입니다. 차이점은 await()를 호출할 필요가 없습니다. 결과가 리턴될 때까지 기다립니다.

    비동기 스타일 함수

    GlobalScope에서 async 코루틴 빌더를 사용하여 명시적으로 비동기 형식의 함수를 만들 수 있습니다.

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

    위 함수는 suspend function이 아닙니다. 어디서든 호출되어 사용가능합니다.

     

    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

    실제로 값을 얻기 위해 await()는 runBlocking{..}에서 받아옵니다.

     

    val one = somethingUsefulOneAsyunc()와 one.await() 사이에서 예외가 발생하면 프로그램이 종료됩니다.

    외부의 try-catch로 인해 에러 처리는 할 수 있으나, 비동기 작업이 유지된 채로 남습니다.

     

    이런식의 비동기 함수 사용은 코틀린 코루틴에서는 비추천 하고 있습니다.

     

    아래와 같은 함수를 만들면 메모리 leak에 안전할 수 있습니다.

    code
    fun main() = runBlocking {
        val time = measureTimeMillis {
            println("The answer is ${concurrentSum()}")
        }
        println("Completed in $time ms")
    }
    
    suspend fun concurrentSum(): Int = coroutineScope {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
         one.await() + two.await()
    }

     

    async 코루틴 빌더는 Coroutine Scope의 extention funcion 입니다.

    따라서 위처럼 부모 Scope를 새로 만들고 그 안에서 async를 사용하면, 두 개의 async는 같은 부모 Scope에서 수행된 작업들이 됩니다.

    따라서 Scope 안에서 예외가 발생하면 해당 Scope을 벗어나 해당 Coroutine Scope에서 수행되었던 자식 코루틴들도 다 취소됩니다.

    code
    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

     

    반응형

    댓글

Designed by Me.