Kotlin/Coroutine

(Coroutine) 3. async

jaesungLeee 2022. 9. 14. 15:02

1. suspend 함수의 순차적 수행

아래 예시를 통해 suspend 함수를 순차적으로 수행해보자.

 

suspend fun getRandom1(): Int {
  delay(1000L)
  return Random.nextInt(0, 500)
}

suspend fun getRandom2(): Int {
  delay(1000L)
  return Random.nextInt(0, 500)
}

fun main() = runBlocking {
  val elapsedTime = measureTimeMillis {
    val value1 = getRandom1()
    val value2 = getRandom2()

    println("$value1 + $value2 = ${value1+value2}")
  }
  println(elapsedTime)
}

>> 82 + 432 = 514
>> 2052

 

suspend 함수인 getRandom1getRandom2는 순차적으로 호출된다. 

각각 1000ms 씩 지연된 후 랜덤 값을 반환하기 때문에 총 2000ms 정도의 시간이 걸릴 것이다.

중요한 점은 두 함수는 순차적으로 실행되기 때문에 무조건 getRandom1이 먼저 실행될 것이다.

한 번에 호출되면 더 효율적이지 않을까?

 

2. async

위 예시에서 사용된 getRandom1getRandom2는 서로의 결과 값에 어떠한 영향도 주지 않는다.

두 함수가 순차적으로 수행되었던 이유는 하나의 코루틴에서 두 함수가 순차적으로 호출되었기 때문이다.

만약, 별도의 코루틴을 만들어 두 함수가 서로 다른 코루틴 상에서 수행된다면 한 번에 수행될 수 있을 것이다.

이때 사용할 수 있는 방법은 async이다.

 

async는 동시에 다른 블록을 수행할 수 있다.

이전에 우리는 launch라는 코루틴 빌더에 대해 알아 봤었다.

asynclaunch의 가장 큰 차이점은 수행 결과 값을 받을 수 있냐 없냐이다.

단순히 launch는 동시에 다른 블록을 수행할 수 있는 반면, async는 수행 후 결과 값을 받을 수 있다.

async는 수행 결과를 await를 통해 받을 수 있다.

 

아래 예시를 살펴보자.

 

suspend fun getRandom1(): Int {
    delay(1000L)
    return Random.nextInt(0, 500)
}

suspend fun getRandom2(): Int {
    delay(1000L)
    return Random.nextInt(0, 500)
}

fun main() = runBlocking {
    val elapsedTime = measureTimeMillis {
        val value1 = async { getRandom1() }
        val value2 = async { getRandom2() }

        println("$value1 + $value2 = ${value1.await()+value2.await()}")
    }
    println(elapsedTime)
}

>> 337 + 102 = 439
>> 1048

 

getRandom1getRandom2async를 통해 동시에 수행시켰다.

두 함수는 모두 별도의 코루틴에서 호출될 것이다.

이 후, value1value2의 수행 결과를 획득하기 위해 await를 사용한다.

 

awaitSuspension point이기 때문에 value1.await을 하게되면 value1에 해당하는 async 블록이 수행될 때 까지 suspend 되었다가 수행이 완료되면 다시 재개하고 반환 값을 받는다.

그렇기 때문에 value2에 해당하는 getRandom2가 동시에 수행될 수 있는 것이다.

출력된 전체 수행시간을 보면 2000ms가 아닌 1000ms대로 수행이 완료된 것을 확인할 수 있다.

 

만약, async 대신 launch를 사용한다면 결과 값을 받아올 수 없어 value1value2에 할당하지 못했을 것이다.

async를 사용하는 이유는 1. 동시에 수행하기 위해서, 2. 결과 값을 가져오기 위해서 로 이해할 수 있을 것이다.

 

3. Lazy async

async를 사용하는 시점부터 해당 코드 블록이 수행되도록 예약된다.

하지만 어떤 블록은 수행이 늦게 되도록 예약하고 싶을 수도 있다.

바로 실행할게 아니라면 이 대기시간 또한 낭비이기 때문이다.

이 경우, asyncstartCoroutineStart.LAZY를 넣을 수 있다.

 

아래 예시를 살펴보자.

 

suspend fun getRandom1WithLazyAsync(): Int {
    delay(1000L)
    return Random.nextInt(0, 500)
}

suspend fun getRandom2WithLazyAsync(): Int {
    delay(1000L)
    return Random.nextInt(0, 500)
}

fun main() = runBlocking {
    val elapsedTime = measureTimeMillis {
        val value1 = async(start = CoroutineStart.LAZY) { getRandom1() }
        val value2 = async(start = CoroutineStart.LAZY) { getRandom2() }

        value1.start()
        value2.start()

        println("${value1.await()} + ${value2.await()} = ${value1.await() + value2.await()}")
    }
    println(elapsedTime)
}

>> 12 + 84 = 96
>> 1027

 

 

4. async를 이용한 코루틴의 구조적 동시성

우리는 이전 포스팅을 통해 코루틴은 계층적인 구조로 이루어져있다고 알게되었다.

계층적인 구조이기 때문에 부모 코루틴과 자식 코루틴의 관계가 중요하다.

 

가끔은 코드를 작성하다보면 예기치못한 예외가 발생할 수 있다.

설명한 것 처럼 코루틴은 계층적 구조를 가지기 때문에 기본적으로 예외가 발생하면 부모 코루틴과 자식 코루틴 모두에게 예외가 전파되어 해당 코루틴 스코프가 취소된다.

 

아래 예시를 살펴보자.

 

suspend fun getRandom1(): Int {
    try {
        delay(1000L)
        return Random.nextInt(0, 500)
    } finally {
        println("getRandom1 is cancelled")
    }
}

suspend fun getRandom2(): Int {
    delay(500L)
    throw IllegalStateException()
}

suspend fun doSomething() = coroutineScope {
    val value1 = async { getRandom1() }
    val value2 = async { getRandom2() }
    
    try {
        println("${value1.await()} + ${value2.await()} = ${value1.await() + value2.await()}")
    } finally {
        println("doSomething is cancelled.")
    }
}

fun main() = runBlocking {
    try {
        doSomething()
    } catch (e: IllegalStateException) {
        println("doSomething failed: $e")
    }
}

>> getRandom1 is cancelled
>> doSomething is cancelled.
>> doSomething failed: java.lang.IllegalStateException

 

getRandom1의 경우 기본적으로 랜덤 넘버를 발생시키지만 예외가 발생할 경우 finally가 수행될 것이다.

getRandom2에서는 의도적으로 IllegalStateException을 발생시킨다.

부모 코루틴 격인 doSomething에서는 이전에 살펴본 것 처럼 getRandom1getRandom2를 동시에 실행시킨 다음 결과를 출력할 것이다.

 

최종 실행 결과를 보면 getRandom2에서 발생한 IllegalStateException이 형제 코루틴인 getRandom1에도 전파되고, 부모 코루틴인 doSomething에도 전파되어 취소되었다.

이는 코루틴이 계층적인 구조로 이루어져있기 때문이다.

기본적으로 코루틴은 예외가 발생하면 해당 코루틴은 cancel될 뿐만 아니라 최종적으로는 가장 상단인 조부모격 코루틴까지도 예외가 전파된다.

 

async로는 여러가지 일을 동시에 처리할 수 있다.

하지만 예외가 발생할 경우 다른 작업에도 전파되어 해당 작업이 취소되기도 한다.

그렇기 떄문에, 코루틴의 계층적 구조는 어떻게 보면 장점이 될 수도 단점이 될 수도 있다.

이러한 예외처리를 위해 코루틴은 CEH : CoroutineExceptionHandler를 제공하여 효율적으로 예외를 관리할 수 있게 한다.

 

Reference

https://kotlinlang.org/docs/coroutines-overview.html

 

Coroutines | Kotlin

 

kotlinlang.org