[Android(Kotlin)] 코루틴(Coroutine) 다시 공부하기 - 3 (취소 및 시간 초과)

업데이트:

카테고리:

태그: , ,


코루틴 취소(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”을 계속 출력하는 것을 볼 수 있다.

위의 코드를 취소를 할 수 있게 만드는 방법에는 두 가지가 있다.

  1. yield 함수
  2. 취소 상태를 명시적으로 확인

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으로 잘나오는뎁쇼?

연구를 좀 해봐야겠다.




참고 문헌