[coroutine] 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 의 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
'프로그래밍 놀이터 > Kotlin, Coroutine' 카테고리의 다른 글
[coroutine] Asyncronous Flow (0) | 2020.03.11 |
---|---|
[coroutine] Composing Suspending Functions ( suspending 함수 만들기 ) (0) | 2020.03.10 |
[coroutine] coroutineScope 의 동작 특성을 알아보자. (0) | 2020.03.08 |
[coroutine] Coroutine Basics ( 코루틴 기초 ) (4) | 2020.03.07 |
Coroutine 과 놀아보기 #2 (2) | 2019.05.23 |
댓글