본문 바로가기
Android/Kotlin

[Kotlin/Coroutine] 3. Composing suspending functions

by 겸 2023. 10. 23.

중단 함수 구성에 대한 다양한 접근법

Sequential by default

순차적으로 실행

 

두개의 중단 함수가 다른 곳에 정의되어있다고 가정하자.

 

코드

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
}

위의 두가지 중단함수가 순차적으로 실행되기 위해서는 어떻게 해야할까?

 

코드

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

실행 결과

The answer is 42
Completed in 2015 ms

일반 코틀린 코드와 마찬가지로 순서대로 호출하면 된다.

 

그렇다면 동시에 실행하도록 하려면 어떻게 하면 될까?


Concurrent using async

두개의 중단 함수 사이에 의존성 없이 우리가 원하는 결과를 더 빨리 얻기위해서 동시에 실행해보자!

async vs launch

공통점

  • 다른 코루틴들과 동시에 별도의 코루틴을 시작한다.

차이점

  • launch
    • job을 리턴
    • 결괏값은 리턴하지 않음
  • async
    • Deferred를 리턴

Deferred

나중에 결과를 제공하겠다는 약속을 나타냄. 비동기 미래를 의미

.awit()를 이용해서 최종 결과를 얻을 수 있다.

DeferredJob에 해당하므로 취소할 수 있다.

 

코드

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

실행 결과

The answer is 42
Completed in 1024 ms

두 개의 코루틴이 동시에 실행되므로 속도가 두 배 빨라진 것을 확인할 수 있다.


Lazily started async

선택적으로 async는 start파라미터를 CoroutineStart.LAZY로 설정함으로써 lazy하게 만들 수 있다.

이 모드는 await에 의해 결과가 호출되거나 Jobstart()로 호출될 때 코루틴이 시작된다.

 

코드

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

실행 결과

The answer is 42
Completed in 1017 ms

두개의 코루틴이 정의되어 있지만, start를 호출하는 정확한 시점이 프로그래머에세 제공된다.

먼저 one이 시작되고 two가 시작된 후 개별 코루틴이 끝날 때까지 기다린다.

 

코드

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

실행 결과

The answer is 42
Completed in 2031 ms

만약 start를 호출하지 않고, await를 호출한다면 코루틴 실행을 시작하고 완료를 기다리기 때문에 순차적인 동작이 일어난다!

async(start = CoroutinueStart.LAZY)는 값의 계산에 중단 함수가 포함된 경우 표준 lazy함수를 대체하는 것이다.


Structured concurrency with async

async를 이용한 구조화된 동시성

두 개의 코루틴을 동시에 수행하고 결과의 합을 리턴하는 함수를 추출한 것이다.

async 코루틴 빌더는 CoroutineScope의 확장으로서 정의되므로 coroutineScope함수 내에 존재해야 한다.

 

코드

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

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

실행 결과

The answer is 42
Completed in 1027 ms

concurrentSum함수 내부에서 문제가 발생하면 해당 스코프에서 시작된 모든 코루틴이 취소된다.

취소는 코루틴 계층 구조에 따라 전파된다.

 

코드

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

실행 결과

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

자식 중 하나(two)가 취소되면 같은 스코프 내에 있는 one 코루틴과 부모 코루틴까지 취소된 것을 볼 수 있다.

반응형