반응형

이 게시물은 다음 링크를 참조하여 학습했습니다

 

Cancellation and timeouts | Kotlin

 

kotlinlang.org

 

Job

Job common A background job. Conceptually, a job is a cancellable thing with a life-cycle that culminates in its completion. Jobs can be arranged into parent-child hierarchies where cancellation of a parent leads to immediate cancellation of all its childr

kotlin.github.io

 

지난 게시물에서 launch{ ... } 는 Job 객체를 반환한다고 설명했다.

그런데 launch를 하는 과정에서 어떠한 이유로 인해 응답이 너무 길어지거나 응답을 무한히 못받는 상황이 발생할 수도 있다.

이를 예방하기 위해서 job을 cancel 하거나, timeout을 걸어주어야 하는데, 이번 게시물은 이에 대한 내용이다.

 

1. Job

먼저 Job에서 제공하는 함수, 프로퍼티들을 정리하려 한다.

 

1-1. Functions

abstract fun cancel() : job을 취소한다.

abstract fun join() : job이 완료될 때까지 코루틴을 일시 중단한다.

abstract fun start(): Boolean : job이 아직 시작되지 않았다면 job을 시작한다. 이미 시작되어있을때는 true, 아니면 false를 반환한다.

suspend fun Job.cancelAndJoin() : cancel(), join()을 순차적으로 실행한다.

 

1-2. Properties

abstract val isActive: Boolean : job이 active 상태인지를 반환한다.

abstract val isCancelled: Boolean : job이 어떤 이유로 취소되었을 때 true를 반환한다. isCancelled는 하위 job이 있을수 있기 때문에 isCompleted와 같지 않다.

abstract val isCompleted: Boolean : job이 어떤 이유로 완료되었을 때 true를 반환한다. job이 취소되었어도 완료되었다고 간주하고 true를 반환한다.

 

2. Cancellation

 

다음은 일반적으로 job을 cancel, join하는 코드다.

import kotlinx.coroutines.*

fun main() = runBlocking {

    val job = launch {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    }

    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancel()
    job.join()
    println("main: Now I can quit")
}

 

위 코드는 아래와 같은 결과를 나타낸다.

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

 

아래는 비슷한 예제지만 repeat을 사용하지 않고, while문을 통해 i를 5번 출력하는 예제이다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch (Dispatchers.Default) {
        try{
            var nextPrintTime = startTime
            var i = 0
            while( i < 5 ){

                if(System.currentTimeMillis() >= nextPrintTime){
                    kotlin.io.println("job: I'm sleeping ${i++} ...")
                    nextPrintTime += 500L
                }
            }
        }catch (e: Exception){
            kotlin.io.println("Exception [$e]")
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    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]
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
main: Now I can quit [main]

 

처음 예제처럼 반복문이 세번만 돌길 바랬지만, while문을 모두 돈것을 볼 수 있다.

 

2-1. suspend fun

이러한 문제는 job객체는 cancel할때 suspend, 즉 지연이 일어나는 시점에서 cancel 할지를 결정하는데, 위 코드에는 suspend fun이 없기 때문이다.

( delay()는 suspend fun이다!)

 

그래서 코드를 아래처럼 바꿔주면 우리가 원하는 결과를 얻을 수 있다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch (Dispatchers.Default) {
        try{
            var nextPrintTime = startTime
            var i = 0
            while( i < 5 ){

                if(System.currentTimeMillis() >= nextPrintTime){
                    delay(1L)
                    kotlin.io.println("job: I'm sleeping ${i++} ...")
                    nextPrintTime += 500L
                }
            }
        }catch (e: Exception){
            kotlin.io.println("Exception [$e]")
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit")
}

 

아래처럼 말이다.

또한 아래 코드를 보면 job을 cancel할때는 내부적으로 Exception을 throw 해준다는 점을 알 수있다.

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

 

coroutines에서는 이런 의미없는 suspend fun을 호출하는 것 대신 yield()라는 suspend fun을 제공한다.

그래서 위 예제를 아래처럼 바꿀 수도 있다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch (Dispatchers.Default) {
        try{
            var nextPrintTime = startTime
            var i = 0
            while( i < 5 ){

                if(System.currentTimeMillis() >= nextPrintTime){
                    yield()
                    kotlin.io.println("job: I'm sleeping ${i++} ...")
                    nextPrintTime += 500L
                }
            }
        }catch (e: Exception){
            kotlin.io.println("Exception [$e]")
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit")
}

 

2-2. 명시적으로 상태 체크

위 방법 말고도 cancel 하는 방법이 있는데, isActive 프로퍼티를 사용하는 것이다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0

        while(isActive){

            if(System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit.")
}

 

위 코드는 아래 결과를 출력한다.

job: I'm sleeping 0 ... [DefaultDispatcher-worker-1]
job: I'm sleeping 1 ... [DefaultDispatcher-worker-1]
job: I'm sleeping 2 ... [DefaultDispatcher-worker-1]
main: I'm tired of waiting! [main]
main: Now I can quit. [main]

 

suspend fun 예제와 가장 큰 차이점은 Exception을 throw 하지 않는것이다.

 

3. timeout

코루틴 빌더에서 delay를 통해 job을 cancel 하는 것 말고 시간을 직접 지정해서 종료시키는 방법도 있다.

 

3-1. withTimeout()

아래 예제를 실행하면 아마 에러가 발생할 것이다....

왜냐면 withTimeout은 Exception을 throw하기 때문이다.

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 (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:502)

 

그래서 withTimeout을 사용한다면 try{ ... } catch{ ... } 를 통해 Exception을 처리해주거나 아니면 아래 방법을 사용하면 된다.

 

3-2. withTimeoutOrNull()

그 방법이 바로 withTimeoutOrNull이다.

withTimeoutOrNull은 작업이 완료되었다면 결과값, 아니라면 null을 반환한다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val result = withTimeoutOrNull(1300L){
        repeat(1000){ i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        "Done"
    }
    println("Result is $result")
}

위 코드를 실행하면 아래 결과를 얻을 수 있다.

I'm sleeping 0 ... [main]
I'm sleeping 1 ... [main]
I'm sleeping 2 ... [main]
Result is null [main]

 

반응형

'Legacy' 카테고리의 다른 글

[Kotlin #14] Coroutines - Coroutines under the hood  (0) 2022.06.15
[Kotlin #13] Coroutines - Composing suspending functions  (0) 2022.06.09
[Kotlin #11] Coroutines  (0) 2022.06.09
[Kotlin #10] Null Safety  (0) 2022.06.02
[Kotlin #9] lateinit, lazy  (0) 2022.05.24

+ Recent posts