본문 바로가기
2023년 이전/kotlin

코루틴(Coroutine) - Cancellation and Timeout

by JeongUPark 2020. 9. 13.
반응형

이번에는 코루틴 Cancellation 과 Timeout에 대하여 알아보겠습니다.

 

장시간 동작하는 에플리케이션에서 필요없는 코루틴을 종료할 필요가 있습니다. 예를들어 특정 페이지를 닫으면 그 페이지에서 실행된 코루틴을 취소할 필요가 있습니다. 그럼 이 종료를 어떻게 하는지 알아보도록 하겠습니다.

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("job: 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.")
}

위 코드를 보면 launch 함수가 job으로 반환되고 중간에 job.cancel로 그 job을 cancel하고 있습니다. 그래서 그 결과화면은

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

이렇게 나타날 것입니다. 만일 job.cancel이 없다면 

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
job: I'm sleeping 5 ...
job: I'm sleeping 6 ...
job: I'm sleeping 7 ...

쭉쭊 1000 까지 나올 것 입니다

Cancellation is cooperative

코루틴 코드는 취소에 협력적입니다.(이말은 곧 취소 가능하다는 말인 것 같습니다.) kotlinx.coroutines의 모든 일시 중지 함수는 취소 가능합니다. 그 함수들은 취소 가능 여부를 체크하고 취소되면 CancellationException을 발생합니다.  하지만 코루틴이 계산에서 작동하고 취소를 확인하지 않으면 취소 할 수 없습니다. 

 

import kotlinx.coroutines.*

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
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: 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.")
}

위 코드의 결과는 

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
main: Now I can quit.

위 코드는 job.cancel과 job.join인 대신 job.cancelAndJoin()으로 대처 되었습니다.

cancelAndJoin의 원형을 보면 다음과 같습니다.

public suspend fun Job.cancelAndJoin() {
    cancel()
    return join()
}

정말 cancel과 join이 다 입니다.  cancel 가능 여부를 체크하고 , cancel 후 코루틴이 완료 될 떄 까지 대기 합니다.

 

여기서 체크해볼만한 점은 while로 횟수를 지정해주고 할 경우에는 그 횟수를 모두 완료 후 "Now I can quit" 가 나타나지만, 위의 repeat는 모두 동작하기 전에 종료 됩니다. (아마 repaat 함수는 계산에서 확인하고 취소를 한건데 아래 while은 취소 가능 여부를 체크하지 않아서 취소가 안되는 것으로 보입니다. 왜 그런지 계속 확인해 보겠습니다. )

 

Making computation code cancellable

계산 코드를 취소 가능하게 만드는 방법에는 두 가지가 있습니다. 첫 번째는 취소를 확인하는 일시 중단 함수를 주기적으로 호출하는 것입니다. 그 목적을 위해 좋은 선택 인 yield 함수가 있습니다. 다른 하나는 취소 상태를 명시 적으로 확인하는 것입니다. 후자의 접근 방식을 보여드리겠습니다.

 

import kotlinx.coroutines.*

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("job: 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.")
}

isActive는 CoroutineScope 객체를 통해 코루틴 내에서 사용할 수있는 확장 속성입니다. 그래서 위의 계산 코루틴이 cancel되어 집니다.

Closing resources with finally

취소 가능한 일시 중단 함수는 취소시 CancellationException을 throw하며 일반적인 방법으로 처리 할 수 ​​있습니다. 

import kotlinx.coroutines.*
import java.lang.Exception

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } catch ( e: Exception){

            println("job: I'm Exception ${e.toString()}")
        }finally {
            println("job: 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.")
}

try catch를 사용하여 cancel이 되면 catch에서 exception이 발생 합니다. 그래서 결과를 보면

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm Exception kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@24273305
job: I'm running finally
main: Now I can quit.

 이런 식으로 cancel이 나타나고 exception이 되서 exception문구가 나타나고, 그 다음 마지막 종료전 프린트가  나타납니다.

Run non-cancellable block

위의 코드의 finally 블록에서 일시 중단 함수를 사용하려고하면이 코드를 실행하는 코루틴이 취소되기 때문에 CancellationException이 발생합니다. 정상적으로 작동하는 모든 닫기 작업(closing-operater) (파일 닫기, 작업 취소 또는 모든 종류의 통신 채널 닫기)은 일반적으로 차단되지 않으며 일시 중단 함수들에 포함되지 않기 때문에 일반적으로 문제가되지 않습니다. 하지만, 드물게 코루틴이 취소되었는데 일시중지가 필요할 경우 해당 코드들을 withContext(NonCancellable) {...}  로 감싸서 사용할 수 있습니다.

import kotlinx.coroutines.*

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.")
}
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
job: And I've just delayed for 1 sec because I'm non-cancellable
main: Now I can quit.

이렇게 하면 위와 같은 결과를 볼 수 있는데, job.cancelAndJoin()이 구동되어도 withContext(NonCancellable) 코드 안에있는 동작들은 계속 동작되고 그 후 코루틴이 종료가 됩니다.

 

TimeOut

코루틴의 실행을 취소하는 가장 명백한 실질적인 이유는 실행 시간이 시간 초과를 초과했기 때문입니다. 그래서 withTimeout 함수를 사용하여 일정 시간이 지나면 코루틴을 취소 시킬 수 있습니다.

import kotlinx.coroutines.*

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
	at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:158)
	at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:128)
	at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.common.kt:497)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
	at kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:68)
	at java.base/java.lang.Thread.run(Thread.java:832)

이런 결과를 볼 수 있습니다. 왜냐면 timeout이 1300ms인데 그 이상으로 동작을 하기 때문입니다. 이전에 이런 exception을 보지 못한 이유는 취소 된 코루틴 내부의 CancellationException이 코루틴 완료의 정상적인 이유로 간주되기 때문입니다. 하지만 위 코드에서는 withTimeout을 사용하여 그렇게 되지 못하게 되었습니다.

그러므로 추가 적인 작업을 하기 위해서는 try catch로 감싸거나 withTimeoutOrNull 함수를 사용해야 합니다. withTimeoutOrNull 은 withTimeOut과 비슷하나, exception이 발생하면 null을 반환 합니다.

import kotlinx.coroutines.*

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

이렇게 결과가 나타나고 시간내에 완료가 되면

I'm sleeping 0 ...
Result is Done

이런 결과를 볼 수 있습니다.

반응형