[Android(Kotlin)] 코루틴(Coroutine) 다시 공부하기 - 3 (취소 및 시간 초과)
카테고리: Android(Kotlin)
코루틴 취소(cancel) 및 시간 초과(timeout)
코루틴 실행 취소
실행 시간이 긴 애플리케이션에서는 백그라운드 코루틴에 대한 세밀한 제어가 필요할 수 있다. 예를 들어, 사용자가 코루틴을 시작한 페이지를 닫았을 경우 그 결과가 더 이상 필요하지 않아 작업을 취소해야할 수 있다. launch
는 실행 중인 코루틴을 취소하는 데 사용할 수 있는 job
을 반환한다.
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.
main 함수가 호출되자마자 job.cancel()
로 인해 코루틴이 취소되었기 때문에 다른 출력을 볼 수 없다. cancel()
과 join()
이 결합한 job의 확장 함수 cancelAndJoin()
도 있다.
cancel은 협조적이다??
코루틴에서 실행 취소는 협조적(cooperative)이다. 모든 코루틴의 suspend 함수는 실행 취소를 할 수 있다. suspend 함수는 코루틴의 취소를 확인하고 코루틴의 취소될 경우 CancellationException을 throw한다. 하지만 코루틴이 취소를 확인하지 않고 계산을 진행한다면 코루틴이 취소되지 않는다.
예시)
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.
작업이 자체적으로 완료될 때까지 취소 후에도 “I’m sleeping”을 계속 출력하는 것을 볼 수 있다.
위의 코드를 취소를 할 수 있게 만드는 방법에는 두 가지가 있다.
- yield 함수
- 취소 상태를 명시적으로 확인
2번 예시
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 객체를 통해 코루틴 내부에서 사용할 수 있는 확장 프로퍼티이다.
리소스 닫기
취소 가능한 suspend 함수는 취소를 할 경우 CancellationException을 throw하는 것으로 정상적으로 종료를 진행한다.
예를 들어 try {...} finally {...}
와 코틀린의 use
함수는 코루틴이 취소될 때 정상적으로 종료 작업을 실행한다.
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.")
취소 불가능 블록 실행하기
이전 예제의 finally
블록에서 suspend 함수를 사용하기위한 모든 시도는 해당 코드를 실행하는 코루틴이 취소되므로 CancellationException
을 야기시킨다. 일반적으로 정상적으로 작동하는 모든 close 기능은 일반적으로 blocking되지 않고 어떤 suspend 함수도 포함하지 않으므로 문제가 되지 않는다. 하지만 드물게 취소된 코루틴에서 일시 중지가 필요한 경우 withContext
함수와 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.")
출력
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
코루틴의 실행을 취소하는 가장 확실한 경우는 실행 시간이 너무 길어져 일부 제한 시간을 초과하는 경우이다.
별도의 코루틴을 실행해 해당 job을 수동으로 추적하고 취소할 수 있지만, 코루틴에는 withTimeout
함수가 준비되어 있다.
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
비동기 Timeout과 리소스
withTimeout
에서의 시간초과 이벤트는 해당 블록에서 실행 중인 코드와 비동기적이고 Timeout블록 내부에서 반환되기 직전에도 언제든지 발생할 수 있다.
예를 들어, close()
함수가 있는 Resource라는 클래스가 있다고 가정하자 이 클래스의 생성은 리소스 획득을 close()
함수는 리소스 해제를 모방한다. 이 클래스에서는 카운터를 증가시키고 close()
함수에서 카운터를 감소시켜 해당 클래스가 생성된(리소스의 획득) 횟수를 추적한다.
시간 제한을 짧게 주고 코루틴을 실행한 후 약간의 지연을 준 뒤 withTimeout
불록 내부에서 Resource를 생성(리소스 획득)하고 외부에서 close()
함수(리소스 해제)를 실행해보자
var acquired = 0
class Resource {
init { acquired++ } // Acquire the resource
fun close() { acquired-- } // Release the resource
}
fun main() {
runBlocking {
repeat(100_000) { // Launch 100K 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
}
}
}
// Outside of runBlocking all coroutines have completed
println(acquired) // Print the number of resources still acquired
}
음.. 여기서 항상 0이 출력되지 않아야 한다는데… 나는 항상 0으로 잘나오는뎁쇼?
연구를 좀 해봐야겠다.
참고 문헌