[Android] 코루틴 Cancellation and Timeouts

    1. 코루틴 왜 써야 하는가

    2. 코루틴 기초

     

    에 이어서 이번에는 코루틴의 취소와 타임아웃에 대해 알아보겠습니다

     

     

    코루틴을 정교하게 취소하지 않으면 메모리를 잡아먹기 때문에 메모리릭이 발생합니다.

    코루틴을 취소하는 방법은 cancel를 호출하면 간단하게 처리됩니다.

    그러나 코루틴 자체가 스스로 cancel에 대해서 협조직은 형태로 구현이 되어있어야 합니다.

    그러한 특성들에 대해서 알아보겠습니다

     

     

    [Cancelling coroutine execution]

      
      fun main() = runBlocking {
        val job = launch {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        }
        delay(1300L) // 1.3초 지연
        println("main: 1.3초 지연 후 출력")
        job.cancel() // job cancel
        job.join() // job의 완료를 기다립니다
        println("main: Now I can quit.")
      }
      

    위의 예제는 코루틴을 실행할때 launch function이 반환해주는 job 객체의 cancel을 호출할 수 있고, 호출하면 코루틴이 종료된다는 것을 보여주는 예제 입니다.

     

    여기서 job.cancel()을 호출하지 않는다면 코루틴은 1000번을 반복할 것입니다.

    cancel을 호출하게되면 1.3초 지연후에 job에 대한 작업만 종료됩니다. (runBlocking 에 대한 작업은 상관 X) 

    위에서 말한것과는 달리 코루틴 자체가 스스로 cancel에 대해 협조적인 형태가 아닙니다.

     

    ( job.cancel()과 job.join()은 job.cancelAndJoin() 함수를 이용하여 한번에 처리할 수 있습니다.)

     

     

     

    [Cancelling is cooperative]

      fun main() = runBlocking {
          val startTime = System.currentTimeMillis()
          val job = launch(Dispatchers.Default) {
              var nextPrintTime = startTime
              var i = 0
              while (i < 5) {
                  // 1초에 2번 메시지 출력
                  if (System.currentTimeMillis() >= nextPrintTime) {
                  
                      //yield() - suspend function
                  
                      println("job: I'm sleeping ${i++} ...")
                      nextPrintTime += 500L
                  }
              }
          }
          delay(1300L) // 1.3초 지연
          println("main: I'm tired of waiting!")
          job.cancelAndJoin() // 작업을 취소하고 완료를 기다림
          println("main: Now I can quit.")    
      }
      

    cancelAndJoin - 현재 coroutine에 종료하라는 신호를 보내고, 정상 종료할 때까지 대기한다.

     

    위의 예제는 i값을 0.5초마다 증가시키며 5번 반복됩니다.

    원하는 결과는 1.3초 지연된 후 job: I'm sleeping 0, 1, 2까지만 찍힌후 종료가 되는 것이었지만 그렇지 않았습니다.

    이유는 코루틴 자체가 취소되는것에 협조적이지 않았기 때문입니다.

     

    suspending functions이 불리지 않았기 때문인데, 취소시키기 위한 방법에는 2가지가 있습니다.

    첫 번째는 위의 예제에 주석이 달려있는 yield function 입니다.

     

    yield는 주기적으로 취소를 체크하는 suspend function을 invoke 시킵니다.

    yield function을 사용하게 되면 원했던 결과값이 출력됩니다.

     

    두 번째는 다음 예제에서 확인해 보겠습니다.

     

    • 코루틴에서 제공하는 모든 suspending functions는 취소를 지원합니다.
    • 코루틴이 취소되면, CancellationException을 발생 시킵니다.

     

     

     

    [Making computation code cancellable]

      fun main() = runBlocking {
          val startTime = System.currentTimeMillis()
          val job = launch(Dispatchers.Default) {
              var nextPrintTime = startTime
              var i = 0
              while (isActive) { // 취소 가능한 루프
                  // 1초에 메시지를 두번 출력
                  if (System.currentTimeMillis() >= nextPrintTime) {
                      println("job: I'm sleeping ${i++} ...")
                      nextPrintTime += 500L
                  }
              }
          }
          delay(1300L) // 1.3초 지연
          println("main: I'm tired of waiting!")
          job.cancelAndJoin() // 작업을 취소하고 완료를 기다림
          println("main: Now I can quit.")    
      }
      

    두번째 방법은 isActive 를 사용하는 방법입니다.

    isActive는 확장된 property로, CoroutineScope object를 통하여 coroutine code 내부에서 사용할수 있습니다.

    isActive는 CancellationException을 던지진 않습니다.

    isActive는 명시적으로 취소 상태를 체크하여 취소시켜줍니다.

     

     

     

    [Closing resources with finally]

      
      fun main() = runBlocking {
          val job = launch {
              try {
                  repeat(1000) { i ->
                      println("job: I'm sleeping $i ...")
                      delay(500L)
                  }
              } finally {
                  println("job: I'm running finally")
              }
          }
          delay(1300L) // 1.3초 지연
          println("main: I'm tired of waiting!")
          job.cancelAndJoin() // 작업을 취소하고 완료를 기다림
          println("main: Now I can quit.")    
      }
      

    coroutine을 cancel 할때 suspend 함수가 재개되면서 exception을 발생시키게 되는데 그 위치에서 리소스를 해제하면 된다는것을 알려주는 예제 입니다.

     

    coroutine을 취소했을때, resource를 닫는다거나, 마지막으로 해야할 작없이 필요할때는 finally 구문을 이용하면 됩니다.

    또한 코틀린에서 resource 해지를 위해 사용하는 use function도 coroutine 취소시 정삭적으로 동작합니다.



     

     

    [Run non-cancellable bloc]

      
      fun main() = runBlocking {
          val job = launch {
              try {
                  repeat(1000) { i ->
                      println("job: I'm sleeping $i ...")
                      delay(500L)
                  }
              } finally {
                  withContext(NonCancellable) {
                      println("job: I'm running finally")
                      delay(1000L)
                      println("job: And I've just delayed for 1 sec because I'm non-cancellable")
                  }
              }
          }
          delay(1300L) // delay a bit
          println("main: I'm tired of waiting!")
          job.cancelAndJoin() // cancels the job and waits for its completion
          println("main: Now I can quit.")    
      }
      

    이 예제는 특수한 케이스 입니다

    이미 cancel된 코루틴 내부에서 suspend function 불러야 되는 경우입니다

    finally 내부에서 다시 suspend function을 사용한다면 CancellationException이 발생하게 됩니다

    이전 예제처럼 네트워크나 파일을 해제 하는 작업은 suspending function을 포함하지 않기에 상관없지만

    이 예제처럼 사용하게 된다면 withContext와 NonCancellable 이라는 coroutine context로 만들어 줘야 합니다

     

    종료된 코루틴 안에서도 다시 코루틴을 불러서 작업을 할 수 있다는 방법을 알려주는 예제였습니다

     

     

     

    [Timeout]

      
      fun main() = runBlocking {
          withTimeout(1300L) {
              repeat(1000) { i ->
                  println("I'm sleeping $i ...")
                  delay(500L)
              }
          }
      }
      

    이 예제는 1.3초 후에 timeout이 걸립니다.

    일일이 Job으로 cancel하지 않고, 특정 시간이후에 취소하도록 하려면 withTimeout을 사용하면 됩니다.

    따로 launch를 만들지 않고 runblocking에서 실행했기 때문에 CancellationException을 발생시키면서 main()이 끝납니다.

     

    withTimeout을 쓸 때는 try-catch 처리를 하지 않으면 앱이 중지되기 때문에 try-catch를 사용해야 합니다

     

     

      
      fun main() = runBlocking {
          val result = withTimeoutOrNull(1300L) {
              repeat(1000) { i ->
                  println("I'm sleeping $i ...")
                  delay(500L)
              }
              "Done" // will get cancelled before it produces this result
          }
          println("Result is $result")
      }
      

    withTimeoutOrNull을 사용하게 되면 exception 대신 null을 반환하게 됩니다

     

     

     

     

     

     

     

     

    references :

     

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

    www.youtube.com/channel/UCJeARDV434voq3IxRTBfzLw

     

    댓글