Hanbit the Developer

Kotlin Documentation | Cancellation and timeouts 본문

Mobile/Kotlin

Kotlin Documentation | Cancellation and timeouts

hanbikan 2023. 5. 24. 17:10

Kotlin Documentation 시리즈에 대해

Category: Official libraries - Coroutines

문서 링크: https://kotlinlang.org/docs/cancellation-and-timeouts.html


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

Cancellation is cooperative

코루틴 취소는 협력적이고 코루틴 코드는 취소가 가능하도록 협조적이어야 한다. kotlinx.coroutines의 모든 정지 함수는 취소할 수 있다. 해당 함수들은 코루틴 취소를 체크하여 취소될 경우 CancellationException을 던진다. 하지만 코루틴이 계산을 진행 중이고 취소를 체크하지 않는다면 취소되지 않는다:

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.

또한 아래와 같이 예외 처리를 할 경우 문제가 발생한다. 이때 취소 체크는 delay(kotlinx.coroutines) 함수가 하여 예외가 던져진 것이다.

val job = launch(Dispatchers.Default) {
    repeat(5) { i ->
        try {
            // print a message twice a second
            println("job: I'm sleeping $i ...")
            delay(500)
        } catch (e: Exception) {
            // log the exception
            println(e)
        }
    }
}
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!
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@7b68429b
job: I'm sleeping 3 ...
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@7b68429b
job: I'm sleeping 4 ...
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@7b68429b
main: Now I can quit.

Making computation code cancellable

  • yield() 함수를 두는 방법
  • 명시적으로 취소 상태를 검사하는 방법:
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.")
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.

Closing resources with finally

val job = launch {
    try {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    } 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.")
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
main: Now I can quit.

Run non-cancellable block

코루틴이 취소된 후 정지 함수를 호출해야 하는 상황이라면 NonCancellable을 사용할 수 있다:

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

Timeout

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 (Coroutine boundary. (:-1) 
 at FileKt$main$1$1.invokeSuspend (File.kt:-1) 
 at FileKt$main$1.invokeSuspend (File.kt:-1) 
Caused by: kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
at kotlinx.coroutines.TimeoutKt .TimeoutCancellationException(Timeout.kt:184)
at kotlinx.coroutines.TimeoutCoroutine .run(Timeout.kt:154)
at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask .run(EventLoop.common.kt:508)

타임아웃 시 TimeoutCancellationException(CancellationException의 서브 클래스)이 발생한다.

예외를 처리하기 위해 try {...} catch (e: TimeoutCancellationException)로 묶거나 withTimeoutOrNull()을 호출할 수 있다.

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

Asynchronous timeout and resources

열었으면 닫아야 하는 리소스가 있는데, 관련 작업을 코루틴이 언제 취소될지 모르는 withTimeout 내에서 진행한다면 언제나 리소스가 방출된다는 것을 보장할 수 없다. 예시는 다음과 같다:

repeat(10_000) { // Launch 10K coroutines
    launch { 
        val resource = withTimeout(60) { // Timeout of 60 ms
            delay(50) // Delay for 50 ms
            Resource() // Acquire a resource and return it from withTimeout block     
        }
        resource.close() // Release the resource
    }
}

위 코드에서 close() 함수가 항상 호출되지는 않는다.(Resource 객체를 생성하는 데 10ms를 초과하는 시간이 걸린 경우)

이를 해결하기 위해 다음과 같은 코드를 작성할 수 있다:

repeat(10_000) { // Launch 10K coroutines
    launch { 
        var resource: Resource? = null // Not acquired yet
        try {
            withTimeout(60) { // Timeout of 60 ms
                delay(50) // Delay for 50 ms
                resource = Resource() // Store a resource to the variable if acquired      
            }
            // We can do something else with the resource here
        } finally {  
            resource?.close() // Release the resource if it was acquired
        }
    }
}