본문 바로가기
프로그래밍 놀이터/Kotlin, Coroutine

[coroutine] Cancellation and Timeouts ( 취소와 타임아웃 )

by 돼지왕 왕돼지 2020. 3. 9.
반응형

[coroutine] Cancellation and Timeouts ( 취소와 타임아웃 )



https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/cancellation-and-timeouts.md#cancellation-and-timeouts


Cancelling coroutine execution


-

오랜 시간 운영되는 앱의 경우 bg coroutine 을 잘 컨트롤 할 필요가 있다.

예를 들어 user 가 coroutine 을 수행시킨 page 를 닫고, 해당 작업의 결과는 더 이상 필요없다면, 해당 작업을 취소시킬 필요가 있다.

"launch" 함수는 Job 을 return 하며, 이 녀석은 동작중인 coroutine 을 취소시킬 수 있다.

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 ...
// job: I'm sleeping 1 ...
// job: I'm sleeping 2 ...
// main: I'm tired of waiting!
// main: Now I can quit.

job.cancel 을 호출하자마자 coroutine 의 추가 결과를 볼 수 없다. 취소되었기 때문이다.

Job 의 extension function 인 cancelAndJoin 함수도 있는데, 이 녀석은 cancel 과 join 을 합쳐놓은 녀석이다.





Cancellation is cooperative.


-

Coroutine 의 취소는 협조적이다. (cooperative)

coroutine code 는 취소에 협조적이어야 한다.

kotlinx.coroutines 의 모든 suspending function 은 취소 가능하다.

해당 함수들은 coroutine 의 취소를 체크하고, 취소가 되었다면 CancellationException 을 던진다.

만약 coroutine 이 계산을 하고 있고, 그래서 취소를 체크할 수 없다면, cancel 을 호출해도 취소되지 않는다.

fun main() = runBlocking{
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default){
        var nextPrintTime = startTime
        var i = 0
        while( i < 5 ){
            if (System.currentTimeInMillis() >= 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 ...
// 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.




Making computation code cancellable


-

계산하는 코드를 취소시키는 방법은 두 가지 접근법이 있다.

하나는 주기적으로 suspending function 을 호출해서 취소를 감지하는 방법이다.

"yield" 함수가 있는데 이 함수가 해당 목적으로 적합하다.

다른 방법은 명시적으로 취소 상태를 체크하는 것이다.

fun main() = runBlocking{
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default){
        var nextPrintTime = startTime
        var i = 0
        while( isActive){
            if (System.currentTimeInMillis() >= 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 ...
// job: I'm sleeping 1 ...
// job: I'm sleeping 2 ...
// main: I'm tired of waiting!
// main: Now I can quit.

isActive 는 CoroutineScope 의 extension property 으로, coroutine 안에서 사용할 수 있다.








Closing resources with finally


-

취소가능한 suspending function 은 CancellationException 을 던지며, 해당 exception 은 보통의 방법으로 다뤄져야 한다.

예를 들어 try{ ... } finally { ... } 과 Kotlin 의 use 함수를 이용하여 취소 후 동작을 정의할 수 있다.

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)
    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!
// job : I'm running finally"
// main: Now I can quit.

join 과 cancelAndJoin 모두 finally action 의 수행을 기다린다.





Run non-cancellable block


-

finally 에서 suspending function 을 호출하는 것은 CancellationException 을 야기한다. 왜냐면 해당 코드를 수행하는 coroutine 은 이미 취소되었기 때문이다.

일반적으로 이것은 문제가 아니다. 왜냐면 제대로된 마무리 동작은(file 닫기, job 취소, channel 닫기 등) 보통 non-blocking 이고, 따라서 suspending function 을 부를 이유도 없기 때문이다.

하지만 아주 드물게 취소되었을 때 suspend function 을 호출할 일이 있다면, withContext(NonCancellable) {... } 을 사용하면 된다.

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




Timeout


-

대부분의 실질적인 코루틴의 취소는 실행 시간이 timeout 을 넘겼기 때문이다.

수동으로 Job 을 직접 referencing 하면서 특정 시간 이후에 취소시킬 수도 있지만, withTimeout 이라는 함수가 있으므로 이 녀석을 사용하면 된다.

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

// job: I'm sleeping 0 ...
// job: I'm sleeping 1 ...
// job: I'm sleeping 2 ...
// Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

TimeoutCancellationException 은 withTimeout 에서 던지며, 이 녀석은 CancellationException 의 subclass 이다.

이런 stack trace 를 이전에 본 적이 없는데, 그것은 취소된 코루틴에서 던지는 CancellationException 은 코루틴 종료의 일반적인 동선으로 보기 때문이다.

하지만 withTimeout 함수를 main function 안에서 직접 사용했기 때문에 exception 이 발생하는 것이다.



-

취소는 단순히 exception 이기 때문에 res 들을 추가로 정리할 것이 있다면 timeout 코드를 try{ ... } catch (e: TimeoutCancellationException) { ... } 블럭으로 감쌀 수 있다. 그러나 특별히 추가적으로 할 일이 없다면 withTimeoutOrNull 로 감싸주면 된다. 이 함수는 withTimeout 과 비슷하지만 timeout 시 exception 을 던지는 대신 null 을 return 한다.

fun main() = runBlocking{
    val result = withTimeoutOrNull(1300L){
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        "Done" // cancel 이 안 되면 이 녀석이 return 된다.
    }
    println("Result is $result")
}

// job: I'm sleeping 0 ...
// job: I'm sleeping 1 ...
// job: I'm sleeping 2 ...
// Result is null



반응형

댓글