본문으로 바로가기
반응형


이 글은 아래 링크의 내용을 기반으로 하여 설명합니다.

https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md

또한 예제에서 로그 print시 println과 안드로이드의 Log.e()를 혼용합니다.

Cancelling coroutine execution

coroutine도 취소해야할 경우가 있습니다. backgroud 작업을 진행중에 페이지가 넘어가서 더이상 결과가 필요하지 않다면 취소해야 겠지요?
이를 위해 launch function이 return하는 Job을 이용합니다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancel() // cancels the job
    job.join() // waits for job's completion 
    println("main: Now I can quit.")
}

repeat으로 1000번 반복하지만 1300ms 이후에 취소되므로 아래와 같은 결과가 나타납니다.

I'm sleeping 0 ...

I'm sleeping 1 ...

I'm sleeping 2 ...

main: I'm tired of waiting!

main: Now I can quit.

job을 canceljoin 시켰지만 cancelAndJoin 명령으로 한번에 처리 할 수도 있습니다.


Cancellation is cooperative

kotlinx.coroutines에서 제공하는 모든 suspending functions은 취소를 지원하며, coroutine이 취소되면, CancellationException을 발생시킵니다.

하지만 아래와 같이 연속된 계산작업인 경우에는 취소시키지 않습니다.

 
fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // computation loop, just wastes CPU
            // yield() //여기에 추가하면 정상적으로 취소된다
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    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.")
}
위 코드를 실행하면 while문이 전부(다섯번) 돌면서 로그를 직고 나서야 main()함수가 종료됩니다.

이런 computation code를 취소시키는 방법은 두가지 입니다.

  1. yield function을 이용하여 추기적으로 취소를 체크하는 suspend function을 invoke 시킨다.
  2. 명시적으로 취소 상태를 체크한다. (isActive 이용)
fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) { // cancellable computation loop
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    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.")
}

위 예제처럼 isActivie를 이용하면 loop도 취소 가능합니다.

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


Closing resources with finally

coroutine을 취소했으나, resource를 닫는다거나, 마지막으로 해야할 작없이 필요할때는 try - finally 구문을 이용합니다.
취소시 onCancellationException이 발생하기 때문에 여기서 처리해도 상관 없습니다.)
또한 코틀린에서 resource 해지를 위해 사용하는 use function도 coroutine 취소시 정상적으로 동작합니다.
fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            println("I'm running finally")
        }
    }
    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.")
}
위 코드의 결과는 아래와 같습니다.

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
I'm running finally
main: Now I can quit.

join cancelAndJoin은 finallization action이 완료될 때까지 기다려 줍니다.

Run non-cancellable block

위 예제에서 만약 finally 내부에 다시 suspending function을 사용한다면 cancellationException이 또 발생합니다.
보통 finally에서 파일을 닫거나, 통신 channel을 해제하는 작업은 suspending function을 포함하지 않기 때문에 상관없습니다만, 부득이 하게 finally에서 suspending function을 사용하야 한다면 withContextNonCancellable context로 감싸줘야 합니다.
fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            withContext(NonCancellable) {
                println("I'm running finally")
                delay(1000L)
                println("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.")
}

Timeout

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

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


위와 같이 하면 아래와 같은 결과가 나타납니다.

I'm sleeping 0 ...

I'm sleeping 1 ...

I'm sleeping 2 ...

Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

실제로 Exception을 발생시키면서 main()이 끝납니다.


안드로이드에서는 try-catch 처리를 하지 않으면 앱이 중지되면서 끝납니다.

위 예제와 같이 cancel() 또는 cancelAndJoin()으로 coroutine을 취소하는 경우에는 try-catch를 하지 않아도 앱은 정상 종료됩니다.

따라서 android에서 withTimeout 쓸때는 꼭 try-catch로 묶어서 사용해야 앱이 중지 되는걸 방지할 수 있습니다.

fun withTimetoutTest() {
        runBlocking {
            try {
                withTimeout(10000L) {
                    repeat(1000) { i ->
                        Log.e(TAG, "I'm sleeping $i...")
                        delay(500L)
                    }
                }
            } catch (te: TimeoutCancellationException) {
                Log.e(TAG, "Timetout!!!")
            }
        }
    }

TimeoutCancellationException은 CancellationException을 상속받아 만들어진 객체 입니다.
따라서 timeout 처리가 필요하다면 try {...} catch (e: TimeoutCancellationException) {...} 구문을 사용하면 되며 withTimeoutOrNull을 이용하면 exception throw 대신 null을 return 합니다.

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


결과는 아래와 같습니다.

I'm sleeping 0 ...

I'm sleeping 1 ...

I'm sleeping 2 ...

Result is null

withTimeoutwithTimeoutOrNull은 코드블럭 내부의{...} 마지막값을 return 합니다.


반응형