[Android] 코루틴 Composing Suspending Functions

    1. 코루틴 왜 써야하는가

    2. 코루틴 기초

    3. 코루틴 Cancellation and Timeouts

     

     

    이번에는 suspending functions을 조합하여 코루틴을 유용하게 사용 하는 법에 대해 알아보겠습니다.

     

     

     

    [Sequential by default]

      
      fun main() = runBlocking<Unit> {
          val time = measureTimeMillis {
              val one = doSomethingUsefulOne()
              val two = doSomethingUsefulTwo()
              println("The answer is ${one + two}")
          }
          println("Completed in $time ms")    
      }
    
      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
      }
      

    이 예제는 코루틴에서도 일반코드와 다름없이 작성돼도 비동기일지라도 순차적으로 실행되는게 기본이라는 것을 알려주는 예제입니다.

     

    이 예제의 결과는 one과 two가 각각 1초씩 지연되고 13과 29의 합이 나오게 되므로 

    The answer is 42, Completed in 2015 ms 가 나오게 됩니다.

     

    • 기본적으로 코드는 Sequential하게 수행됨
    • 코루틴에서도 동일하게 수행됨 

     

     

     

    [Concurrent using async]

      
      fun main() = runBlocking<Unit> {
          val time = measureTimeMillis {
              val one = async { doSomethingUsefulOne() }
              val two = async { doSomethingUsefulTwo() }
              println("The answer is ${one.await() + two.await()}")
          }
          println("Completed in $time ms")    
      }
    
      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
      }
      

    이전 예제는 순차적으로 진행돼 1초 + 1초의 지연으로 총 2초정도가 걸렸습니다.

     

    이번 예제는 async를 이용하여 동시 진행을 시켜보는 예제 입니다.

    결과적으로 1초 + 1초 = 1초의 결과를 보입니다.

     

    async를 호출하게되면 one이 실행되고 나서 바로 다음라인인 two로 진행됩니다.

    먼저 끝나는것이 먼저 출력되는게 아닐까 싶지만 await를 붙여줬기 때문에 각 동작이 끝날때까지 기다려 주게 됩니다.

     

    await를 붙이게되면 그 동작이 완료될때까지 기다리게 됩니다

    따라서 await를 이용이 다시 순차적으로 진행이 되게 할수도 있고, 지금처럼 동시 진행이 가능하게 할 수도 있습니다

     

    async를 실행한 결과는 job을 상속한 deferred 객체 입니다.

    job이 join을 한다든지 기다린다든지 cancel을 했던것처럼, 값 하나를 명시적으로 기다릴수 있는 상태가 됩니다

     

    • launch 는 job을 반환한다
    • async 는 Deferred를 반환한다
    • job은 launch 시킨 코루틴의 상태를 관리하는 용도로 사용되고 결과를 return 받을 수 없다
    • Deferred는 async 블럭에서 수행된 결과는 return 받을 수 있다
    • Deferred 또한 job을 상속 받았기 때문에 제어가 가능하다

     

     

     

    [Lazily started async]

      
      fun main() = runBlocking<Unit> {
          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")    
      }
      
      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
      }
      

    선택적 으로 매개 변수를 CoroutineStart.LAZY 로 설정하여 비동기 지연시킬 수 있습니다 .

    작업을 수행하기 위해서 start를 해주거나 await를 호출하면 됩니다 

     

    위의 예제를 start를 해준 상태에서 실행시키면

    The answer is 42

    Completed in 1017 ms

    의 결과를 받을 수 있습니다

     

    start를 지우고 실행시키게 되면

    The answer is 42

    Completed in 2015 ms

    의 결과를 받을 수 있습니다

     

     

     

     

     

    [Async-style functions]

      
      fun main() {
          val time = measureTimeMillis {
              // 코루틴 외부에서 비동기 작업을 시작 할 수 있습니다
              val one = somethingUsefulOneAsync()
              val two = somethingUsefulTwoAsync()
              // 결과를 기다리는 것은 suspending 이나 blocking을 포함해야 합니다
              // runBlocking를 이용하여 결과는 기다리는동안 메인스레드를 차단합니다
              runBlocking {
                  println("The 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) // 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
      }
      

    이번 예제는 이렇게 하면 안된다는 예시를 보여주는 예제입니다

     

    일반 함수를 만들어서 어디서든 사용 할 수 있게 만들어 준 코드입니다

    문제가 없어보이지만 exception이 발생했을때 돌이킬 수 없는 문제가 생기게 됩니다

     

    이 코드를 실행시켜보면 이전과 다를것없이 42와 1017ms 의 결과를 보입니다

    그러나 중간에 exception이 발생하게 됐을때 중단되지 않고 코루틴은 계속 실행하게 됩니다

    GlobalScope에서 실행됐기 때문입니다.

     

    GlobalScope에서 독립적인 형태로 코루틴을 실행시키게되면 exception의 핸들링이 떨어지기 때문에

    이러한 사용은 코틀린 코루틴에서는 강력하게 비추천 하고 있습니다 

     

     

     

    [Suructured concurrency with async]

      
      fun main() = runBlocking<Unit> {
          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()
      }
    
      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
      }
      

    이번 예제는 이러한 형태로 사용할 것을 권장하는 예제입니다.

     

    이번예제는 코루틴 스코프 안에서 exception이 발생하기 때문에 모든 코루틴이 취소가 될 것 입니다.

    Suructured concurrency 의 형태로 suspending function을 조합하여 코루틴을 구성하는것을 권장합니다

     

     

     

     

    [Cancellation propagated coroutine hierarchy]

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

    이 예제는 부모 코루틴 안에 자식 코루틴 2개가 있는 형태 입니다

    두번째 코루틴에서 exception이 발생하게 되면 cancel이 전파되고 모든 코루틴이 정상적으로 종료되게 됩니다

     

    만약 중간에 GlobalScope를 넣었다면 이 동작은 취소되지 않을 것 입니다.

     

    structured concurrency를 위해서 예제와 같은 형태의 쓰임을 권장합니다!



     

     

     

     

     

    references :

    kotlinlang.org/docs/reference/coroutines/coroutines-guide.html

    www.youtube.com/channel/UCJeARDV434voq3IxRTBfzLw

    댓글