본문 바로가기
프로그래밍 놀이터/Kotlin, Coroutine

[coroutine] Composing Suspending Functions ( suspending 함수 만들기 )

by 돼지왕 왕돼지 2020. 3. 10.
반응형

[coroutine] Composing Suspending Functions ( suspending 함수 만들기 )



https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/composing-suspending-functions.md#composing-suspending-functions


Sequential by default


-

2개의 suspending function 이 있다고 가정해보자.

그리고 그 두 개의 함수가 remote service call 이나 계산을 하는 등의 의미있는 행동을 하며, 시간이 좀 걸린다고 해보자.

suspend fun doSomethingUsefulOne(): Int{
    delay(1000L) // 의미있는 행동이라 가정
    return 13
}

suspend fun doSomethingUsefulTwo(): Int{
    delay(1000L) // 의미있는 행동이라 가정
    return 29
}

위 두개의 함수가 순서대로 호출되길 원한다면 뭘해야 할까? 

doSomethingUsefulOne 가 먼저 불리고 doSomethingUsefulTwo 가 불리고.. 그 다음 결과값을 더하고 싶다면?

첫번째 함수 결과가 두번째 함수를 수행하는데 반영한다면 이런 순서대로 호출이 꼭 필요할 것이다.


coroutine 안에서 기본적으로 순서대로 쓰인 코드는 기본적으로 순서대로 수행된다.

fun main() = runBlocking<Unit>{
    val time = measureTimeMillis{
        val one = doSomethingUserfulOne()
        val two = doSomethingUsefulTwo()
        println("The answer is ${one + two}")
    }
    println("Completed in $time ms")
}

suspend fun doSomethingUsefulOne(): Int{
    delay(1000L)
    return 13
}

suspend fun doSomethingUsefulTwo(): Int{
    delay(1000L)
    return 29
}

// Thw answer is 42
// Completed in 2017 ms




Concurrent using async


-

만약에 doSomethingUsefulOne 과 doSomethingUsefulTwo 가 상관관계가 없고, 동시 수행해서 더 빨리 답을 얻고싶다면 어떻게 해야 할까?

"async" 가 도움일 줄 것이다.


개념상으로 async 는 launch 와 비슷하다.

별개의 light-weight thread 인 coroutine 을 시작하고, 이 coroutine 은 다른 coroutine 들과 병렬적으로 작동한다.

차이가 있다면 launch 는 Job 을 return 하고 Job 은 결과값을 반영하지 않지만, async 는 light-weight non-blocking future인 Deferred 를 return 하고 이를 통해 결과값을 얻을 수 있다는 것이다.

.await() 함수를 통해 결과를 얻어올 수 있다.

그리고 Deffered 도 사실은 Job 이기 때문에 취소도 가능하다.

fun main() = runBlocking<Unit>{
    val time = measureTimeMillis{
        val one = async { doSomethingUserfulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

suspend fun doSomethingUsefulOne(): Int{
    delay(1000L)
    return 13
}

suspend fun doSomethingUsefulTwo(): Int{
    delay(1000L)
    return 29
}

// Thw answer is 42
// Completed in 1017 ms

약 2배 빠르게 수행되었다. 왜냐면 2개의 coroutine 이 동시에 수행되었기 때문이다.

명심할 것은 coroutine 을 통한 동시성은 항상 명시적이라는 것이다.




Lazily started async


-

옵션으로 async 는 lazy 하게 시작될 수 있다. 이는 "start" param 에 CoroutineStart.LAZY 를 넘겨줌으로 가능하다.

이 모드에서는 await 함수가 불렸을 때 또는 Job 의 start 함수가 불렸을 때 coroutine 이 수행된다.

fun main() = runBlocking<Unit>{
    val time = measureTimeMillis{
        val one = async( start = CoroutineStart.LAZY ){ doSomethingUserfulOne() }
        val two = async( start = CoroutineStart.LAZY ){ doSomethingUsefulTwo() }
        one.start()
        two.start()
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

suspend fun doSomethingUsefulOne(): Int{
    delay(1000L)
    return 13
}

suspend fun doSomethingUsefulTwo(): Int{
    delay(1000L)
    return 29
}

// Thw answer is 42
// Completed in 1017 ms

이 예제에서 2개의 coroutine 은 정의되었지만 이전 예제처럼 바로 실행되지는 않는다.

start 를 불러서 실행시킬 수 있는 제어권을 프로그래머가 갖게 된다.


주의할 것은 우리가 start 함수들의 호출 없이 println 안에서 바로 await 를 호출했다면, 이는 순차적 실행을 했을 것이다.

왜냐하면 await 는 coroutine 을 수행하고 그것의 결과를 받아올 때까지 기다리기 때문이다.


async(start = CoroutineStart.LAZY 의 사용처는 계산 함수가 suspending function 을 가질 때 "lazy" 함수를 사용하는 것이 대체제이다.








Async-style functions.


-

우리는 async-style 함수를 정의하고 doSomethingUsefulOne 과 doSomethingUsefulTwo 를 async coroutine builder 를 GlobalScope 안에서 호출하도록 할 수 있다.

우리는 그런 함수를 "...Async" 라는 어미를 붙여서 async 계산이라는 것을 보여주고 있다.

그리고 그 함수 호출은 Deferred 를 return 할 것이라는 것을

fun somethingUsefulOneAsync() = GlobalScope.async{
    doSomethingUsefulOne()
}

fun somethingUsefulTwoAsync() = GlobalScope.async{
    doSomethingUsefulTwo()
}

주의할 것은 xxxAsync 함수들은 suspending function 이 아니라는 것이다.

이것은 어디서든 사용될 수 있으며, 단지 항상 async 라는 것을 보여주는 것 뿐이다.

fun main() {
    val time = measureTimeMillis{
        val one =  somethingUsefulOneAsync()
        val two =  somethingUsefulTwoAsync()
        runBlocking{
            println("Thw answer is ${one.await() + two.await()")
        }
    }
    println("Completed in $time ms")
}

fun somethingUsefulOneAsync() = GlobalScope.async{
    doSomethingUsefulOne()
}

fun somethingUsefulTwoAsync() = GlobalScope.async{
    doSomethingUsefulTwo()
}

suspend fun doSomethingUsefulOne(): Int{
    delay(1000L)
    return 13
}

suspend fun doSomethingUsefulTwo(): Int{
    delay(1000L)
    return 29
}

// Thw answer is 42
// Completed in 1017 ms

여기에 보여준 async function style 은 다른 프로그래밍 언어에서 인기있는 스타일이기에 오직 이렇게 할 수 있음을 보여주기 위함이다.

Kotlin coroutine 에서 이런 스타일로 사용하는 것은 "강력하게 비추" 이다.


val one = somethingUserfulOneAsync() 과 one.await() 사이에 어떤 로직이 들어가 있고, 그 로직이 이 함수 종료를 위해 exception 을 던진다면 무슨 일이 일어날까?

일반적으로 global error-handler 가 이 exception 을 잡고 log 를 찍은 후, 로직을 계속 수행한다.

그러나 이 함수 로직이 중단되었음에도 somethingUsefulOneAsync 는 계속해서 돈다.

이 문제는 아래의 structured concurrency 를 통해 피할 수 있다.





Structured concurrency with async


-

async coroutine builder 는 CoroutineScope 의 extension 으로 정의되어 있기 때문에, coroutineScope 함수를 통해서도 접근할 수 있다.

그리고 extract function 을 아래와 같이 해보자.

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

이 방법으로 concurrentSum 안에서 무언가가 잘못 되어 exception 을 던질 때, 모든 해당 scope 에서 launch 된 coroutine 은 모두 한번에 취소가 된다.


fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        println("The answer is ${concurrentSum(){")
    }
    println("Computed in $time ms")
}

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

suspend fun doSomethingUsefulOne(): Int{
    delay(1000L)
    return 13
}

suspend fun doSomethingUsefulTwo(): Int{
    delay(1000L)
    return 29
}

// Thw answer is 42
// Completed in 1017 ms


-

취소는 항상 coroutine hierarchy 를 통해 propagate(전파) 된다. 

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

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




반응형

댓글